diff --git a/Dockerfile b/Dockerfile index b0b3c857..e3f824bc 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,5 +1,10 @@ # Build a base image with modules cached. FROM --platform=${BUILDPLATFORM} golang:1.23 AS base +ARG TARGETARCH + +# Install tini to reap zombie processes. +ENV TINI_VERSION=v0.19.0 +ADD --chmod=755 https://github.com/krallin/tini/releases/download/${TINI_VERSION}/tini-static-${TARGETARCH} /tini COPY /go.mod go.mod COPY /go.sum go.sum @@ -37,8 +42,11 @@ RUN --mount=type=cache,target=${GOCACHE} \ # Use distroless as minimal base image to package the manager binary # Refer to https://github.com/GoogleContainerTools/distroless for more details FROM gcr.io/distroless/static-debian12:debug-nonroot + +COPY --from=base /tini /tini COPY --from=op-builder /manager /manager COPY --from=agent-builder /agent /agent USER 65532:65532 -ENTRYPOINT ["/manager"] +ENTRYPOINT ["/tini", "--"] +CMD ["/manager"] diff --git a/agent/cmd/serve.go b/agent/cmd/serve.go index fd72a2df..8a1a13fd 100644 --- a/agent/cmd/serve.go +++ b/agent/cmd/serve.go @@ -22,6 +22,7 @@ import ( "os" "os/signal" "path/filepath" + "runtime/debug" "syscall" "github.com/pulumi/pulumi-kubernetes-operator/v2/agent/pkg/server" @@ -30,6 +31,7 @@ import ( "github.com/spf13/cobra" "go.uber.org/zap" "go.uber.org/zap/zapio" + "k8s.io/apimachinery/pkg/api/resource" ) var ( @@ -52,6 +54,15 @@ var serveCmd = &cobra.Command{ log.Infow("Pulumi Kubernetes Agent", "version", version.Version) log.Debugw("executing serve command", "WorkDir", _workDir) + // limit the agent's memory usage to the configured quantity (e.g. 64Mi) + if limit, ok := os.LookupEnv("AGENT_MEMLIMIT"); ok { + val := resource.MustParse(limit) + if !val.IsZero() { + log.Debugf("setting memory limit to %s", limit) + debug.SetMemoryLimit(val.Value()) + } + } + // open the workspace using auto api workspaceOpts := []auto.LocalWorkspaceOption{} workDir, err := filepath.EvalSymlinks(_workDir) // resolve the true location of the workspace diff --git a/deploy/crds/auto.pulumi.com_workspaces.yaml b/deploy/crds/auto.pulumi.com_workspaces.yaml index 303a898c..df9453d2 100644 --- a/deploy/crds/auto.pulumi.com_workspaces.yaml +++ b/deploy/crds/auto.pulumi.com_workspaces.yaml @@ -8413,8 +8413,12 @@ spec: type: object securityProfile: default: restricted - description: SecurityProfile applies a security profile to the workspace, - 'restricted' by default. + description: |- + SecurityProfile applies a security profile to the workspace. + The restricted profile (default) runs the pod as a non-root user and with a security context that conforms with + the Restricted policy of the Pod Security Standards. + The baseline profile runs the pod as the root user and with a security context that conforms with + the Baseline policy of the Pod Security Standards. type: string serviceAccountName: default: default diff --git a/deploy/crds/pulumi.com_stacks.yaml b/deploy/crds/pulumi.com_stacks.yaml index 3549fe59..841d8cfd 100644 --- a/deploy/crds/pulumi.com_stacks.yaml +++ b/deploy/crds/pulumi.com_stacks.yaml @@ -9334,8 +9334,12 @@ spec: type: object securityProfile: default: restricted - description: SecurityProfile applies a security profile to - the workspace, 'restricted' by default. + description: |- + SecurityProfile applies a security profile to the workspace. + The restricted profile (default) runs the pod as a non-root user and with a security context that conforms with + the Restricted policy of the Pod Security Standards. + The baseline profile runs the pod as the root user and with a security context that conforms with + the Baseline policy of the Pod Security Standards. type: string serviceAccountName: default: default @@ -18871,8 +18875,12 @@ spec: type: object securityProfile: default: restricted - description: SecurityProfile applies a security profile to - the workspace, 'restricted' by default. + description: |- + SecurityProfile applies a security profile to the workspace. + The restricted profile (default) runs the pod as a non-root user and with a security context that conforms with + the Restricted policy of the Pod Security Standards. + The baseline profile runs the pod as the root user and with a security context that conforms with + the Baseline policy of the Pod Security Standards. type: string serviceAccountName: default: default diff --git a/deploy/helm/pulumi-operator/crds/auto.pulumi.com_workspaces.yaml b/deploy/helm/pulumi-operator/crds/auto.pulumi.com_workspaces.yaml index 303a898c..df9453d2 100644 --- a/deploy/helm/pulumi-operator/crds/auto.pulumi.com_workspaces.yaml +++ b/deploy/helm/pulumi-operator/crds/auto.pulumi.com_workspaces.yaml @@ -8413,8 +8413,12 @@ spec: type: object securityProfile: default: restricted - description: SecurityProfile applies a security profile to the workspace, - 'restricted' by default. + description: |- + SecurityProfile applies a security profile to the workspace. + The restricted profile (default) runs the pod as a non-root user and with a security context that conforms with + the Restricted policy of the Pod Security Standards. + The baseline profile runs the pod as the root user and with a security context that conforms with + the Baseline policy of the Pod Security Standards. type: string serviceAccountName: default: default diff --git a/deploy/helm/pulumi-operator/crds/pulumi.com_stacks.yaml b/deploy/helm/pulumi-operator/crds/pulumi.com_stacks.yaml index 3549fe59..841d8cfd 100644 --- a/deploy/helm/pulumi-operator/crds/pulumi.com_stacks.yaml +++ b/deploy/helm/pulumi-operator/crds/pulumi.com_stacks.yaml @@ -9334,8 +9334,12 @@ spec: type: object securityProfile: default: restricted - description: SecurityProfile applies a security profile to - the workspace, 'restricted' by default. + description: |- + SecurityProfile applies a security profile to the workspace. + The restricted profile (default) runs the pod as a non-root user and with a security context that conforms with + the Restricted policy of the Pod Security Standards. + The baseline profile runs the pod as the root user and with a security context that conforms with + the Baseline policy of the Pod Security Standards. type: string serviceAccountName: default: default @@ -18871,8 +18875,12 @@ spec: type: object securityProfile: default: restricted - description: SecurityProfile applies a security profile to - the workspace, 'restricted' by default. + description: |- + SecurityProfile applies a security profile to the workspace. + The restricted profile (default) runs the pod as a non-root user and with a security context that conforms with + the Restricted policy of the Pod Security Standards. + The baseline profile runs the pod as the root user and with a security context that conforms with + the Baseline policy of the Pod Security Standards. type: string serviceAccountName: default: default diff --git a/deploy/helm/pulumi-operator/templates/deployment.yaml b/deploy/helm/pulumi-operator/templates/deployment.yaml index e01e5be7..c055fd95 100644 --- a/deploy/helm/pulumi-operator/templates/deployment.yaml +++ b/deploy/helm/pulumi-operator/templates/deployment.yaml @@ -33,13 +33,12 @@ spec: {{- toYaml .Values.extraSidecars | nindent 8 }} {{- end}} - name: manager - command: - - /manager args: + - /manager - --leader-elect - --health-probe-bind-address=:8081 - --metrics-bind-address=:8383 - - --program-fs-adv-addr=pulumi-kubernetes-operator.$(POD_NAMESPACE).svc.cluster.local + - --program-fs-adv-addr=pulumi-kubernetes-operator.$(POD_NAMESPACE).svc.cluster.local:80 - --zap-log-level={{ .Values.controller.logLevel }} - --zap-time-encoding=iso8601 env: diff --git a/deploy/yaml/install.yaml b/deploy/yaml/install.yaml index e5694d5f..4d38710a 100644 --- a/deploy/yaml/install.yaml +++ b/deploy/yaml/install.yaml @@ -9569,8 +9569,12 @@ spec: type: object securityProfile: default: restricted - description: SecurityProfile applies a security profile to - the workspace, 'restricted' by default. + description: |- + SecurityProfile applies a security profile to the workspace. + The restricted profile (default) runs the pod as a non-root user and with a security context that conforms with + the Restricted policy of the Pod Security Standards. + The baseline profile runs the pod as the root user and with a security context that conforms with + the Baseline policy of the Pod Security Standards. type: string serviceAccountName: default: default @@ -19106,8 +19110,12 @@ spec: type: object securityProfile: default: restricted - description: SecurityProfile applies a security profile to - the workspace, 'restricted' by default. + description: |- + SecurityProfile applies a security profile to the workspace. + The restricted profile (default) runs the pod as a non-root user and with a security context that conforms with + the Restricted policy of the Pod Security Standards. + The baseline profile runs the pod as the root user and with a security context that conforms with + the Baseline policy of the Pod Security Standards. type: string serviceAccountName: default: default @@ -27868,8 +27876,12 @@ spec: type: object securityProfile: default: restricted - description: SecurityProfile applies a security profile to the workspace, - 'restricted' by default. + description: |- + SecurityProfile applies a security profile to the workspace. + The restricted profile (default) runs the pod as a non-root user and with a security context that conforms with + the Restricted policy of the Pod Security Standards. + The baseline profile runs the pod as the root user and with a security context that conforms with + the Baseline policy of the Pod Security Standards. type: string serviceAccountName: default: default @@ -28388,14 +28400,13 @@ spec: spec: containers: - args: + - /manager - --leader-elect - --health-probe-bind-address=:8081 - --metrics-bind-address=:8383 - - --program-fs-adv-addr=pulumi-kubernetes-operator.$(POD_NAMESPACE).svc.cluster.local + - --program-fs-adv-addr=pulumi-kubernetes-operator.$(POD_NAMESPACE).svc.cluster.local:80 - --zap-log-level=error - --zap-time-encoding=iso8601 - command: - - /manager env: - name: POD_NAMESPACE valueFrom: diff --git a/operator/Makefile b/operator/Makefile index 620228a0..c9ad6416 100644 --- a/operator/Makefile +++ b/operator/Makefile @@ -145,7 +145,7 @@ build: manifests generate fmt vet ## Build manager binary. .PHONY: run run: manifests generate fmt vet ## Run a controller from your host. - go run ./cmd/main.go --program-fs-adv-addr=localhost:9090 + WORKSPACE_LOCALHOST=localhost:50051 SOURCE_CONTROLLER_LOCALHOST=localhost:9090 go run ./cmd/main.go # If you wish to build the manager image targeting other platforms you can use the --platform flag. # (i.e. docker build --platform linux/arm64). However, you must enable docker buildKit for it. @@ -183,7 +183,7 @@ endif .PHONY: install install: manifests kustomize ## Install CRDs into the K8s cluster specified in ~/.kube/config. - $(KUSTOMIZE) build config/crd | $(KUBECTL) apply --server-side=true -f - + $(KUSTOMIZE) build config/crd | $(KUBECTL) apply --server-side=true --force-conflicts -f - .PHONY: uninstall uninstall: manifests kustomize ## Uninstall CRDs from the K8s cluster specified in ~/.kube/config. Call with ignore-not-found=true to ignore resource not found errors during deletion. diff --git a/operator/api/auto/v1alpha1/workspace_types.go b/operator/api/auto/v1alpha1/workspace_types.go index 4a81069f..e50a5040 100644 --- a/operator/api/auto/v1alpha1/workspace_types.go +++ b/operator/api/auto/v1alpha1/workspace_types.go @@ -34,11 +34,6 @@ const ( SecurityProfileBaseline SecurityProfile = "baseline" // SecurityProfileRestricted applies the restricted security profile. SecurityProfileRestricted SecurityProfile = "restricted" - - // SecurityProfileBaselineDefaultImage is the default image used when the security profile is 'baseline'. - SecurityProfileBaselineDefaultImage = "pulumi/pulumi:latest" - // SecurityProfileRestrictedDefaultImage is the default image used when the security profile is 'restricted'. - SecurityProfileRestrictedDefaultImage = "pulumi/pulumi:latest-nonroot" ) // WorkspaceSpec defines the desired state of Workspace @@ -47,7 +42,11 @@ type WorkspaceSpec struct { // +kubebuilder:default="default" ServiceAccountName string `json:"serviceAccountName,omitempty"` - // SecurityProfile applies a security profile to the workspace, 'restricted' by default. + // SecurityProfile applies a security profile to the workspace. + // The restricted profile (default) runs the pod as a non-root user and with a security context that conforms with + // the Restricted policy of the Pod Security Standards. + // The baseline profile runs the pod as the root user and with a security context that conforms with + // the Baseline policy of the Pod Security Standards. // +kubebuilder:default="restricted" // +optional SecurityProfile SecurityProfile `json:"securityProfile,omitempty"` diff --git a/operator/cmd/main.go b/operator/cmd/main.go index 8b9faa14..0f2be5e0 100644 --- a/operator/cmd/main.go +++ b/operator/cmd/main.go @@ -27,7 +27,6 @@ import ( // Import all Kubernetes client auth plugins (e.g. Azure, GCP, OIDC, etc.) // to ensure that exec-entrypoint and run can make use of them. _ "k8s.io/client-go/plugin/pkg/client/auth" - "sigs.k8s.io/controller-runtime/pkg/client" sourcev1 "github.com/fluxcd/source-controller/api/v1" sourcev1b2 "github.com/fluxcd/source-controller/api/v1beta2" @@ -150,7 +149,10 @@ func main() { // Create a new ProgramHandler to handle Program objects. Both the ProgramReconciler and the file server need to // access the ProgramHandler, so it is created here and passed to both. - pHandler := newProgramHandler(mgr.GetClient(), programFSAdvAddr) + if programFSAdvAddr == "" { + programFSAdvAddr = determineAdvAddr(programFSAddr) + } + pHandler := pulumicontroller.NewProgramHandler(mgr.GetClient(), programFSAdvAddr) if err = (&autocontroller.WorkspaceReconciler{ Client: mgr.GetClient(), @@ -246,14 +248,6 @@ func (fs pFileserver) Start(ctx context.Context) error { } } -func newProgramHandler(k8sClient client.Client, advAddr string) *pulumicontroller.ProgramHandler { - if advAddr == "" { - advAddr = determineAdvAddr(advAddr) - } - - return pulumicontroller.NewProgramHandler(k8sClient, advAddr) -} - func determineAdvAddr(addr string) string { host, port, err := net.SplitHostPort(addr) if err != nil { diff --git a/operator/config/crd/bases/auto.pulumi.com_workspaces.yaml b/operator/config/crd/bases/auto.pulumi.com_workspaces.yaml index 303a898c..df9453d2 100644 --- a/operator/config/crd/bases/auto.pulumi.com_workspaces.yaml +++ b/operator/config/crd/bases/auto.pulumi.com_workspaces.yaml @@ -8413,8 +8413,12 @@ spec: type: object securityProfile: default: restricted - description: SecurityProfile applies a security profile to the workspace, - 'restricted' by default. + description: |- + SecurityProfile applies a security profile to the workspace. + The restricted profile (default) runs the pod as a non-root user and with a security context that conforms with + the Restricted policy of the Pod Security Standards. + The baseline profile runs the pod as the root user and with a security context that conforms with + the Baseline policy of the Pod Security Standards. type: string serviceAccountName: default: default diff --git a/operator/config/crd/bases/pulumi.com_stacks.yaml b/operator/config/crd/bases/pulumi.com_stacks.yaml index 3549fe59..841d8cfd 100644 --- a/operator/config/crd/bases/pulumi.com_stacks.yaml +++ b/operator/config/crd/bases/pulumi.com_stacks.yaml @@ -9334,8 +9334,12 @@ spec: type: object securityProfile: default: restricted - description: SecurityProfile applies a security profile to - the workspace, 'restricted' by default. + description: |- + SecurityProfile applies a security profile to the workspace. + The restricted profile (default) runs the pod as a non-root user and with a security context that conforms with + the Restricted policy of the Pod Security Standards. + The baseline profile runs the pod as the root user and with a security context that conforms with + the Baseline policy of the Pod Security Standards. type: string serviceAccountName: default: default @@ -18871,8 +18875,12 @@ spec: type: object securityProfile: default: restricted - description: SecurityProfile applies a security profile to - the workspace, 'restricted' by default. + description: |- + SecurityProfile applies a security profile to the workspace. + The restricted profile (default) runs the pod as a non-root user and with a security context that conforms with + the Restricted policy of the Pod Security Standards. + The baseline profile runs the pod as the root user and with a security context that conforms with + the Baseline policy of the Pod Security Standards. type: string serviceAccountName: default: default diff --git a/operator/config/manager/manager.yaml b/operator/config/manager/manager.yaml index 02a416c1..d72b2532 100644 --- a/operator/config/manager/manager.yaml +++ b/operator/config/manager/manager.yaml @@ -33,18 +33,18 @@ spec: runAsUser: 65532 runAsGroup: 65532 containers: - - command: - - /manager + - name: manager env: - name: POD_NAMESPACE valueFrom: fieldRef: fieldPath: metadata.namespace args: + - /manager - --leader-elect - --health-probe-bind-address=:8081 - --metrics-bind-address=:8383 - - --program-fs-adv-addr=pulumi-kubernetes-operator.$(POD_NAMESPACE).svc.cluster.local + - --program-fs-adv-addr=pulumi-kubernetes-operator.$(POD_NAMESPACE).svc.cluster.local:80 - --zap-log-level=error - --zap-time-encoding=iso8601 ports: @@ -56,7 +56,6 @@ spec: protocol: TCP image: controller:latest imagePullPolicy: IfNotPresent - name: manager securityContext: allowPrivilegeEscalation: false capabilities: diff --git a/operator/internal/controller/auto/workspace_controller.go b/operator/internal/controller/auto/workspace_controller.go index fd2b593a..f83de2eb 100644 --- a/operator/internal/controller/auto/workspace_controller.go +++ b/operator/internal/controller/auto/workspace_controller.go @@ -22,11 +22,13 @@ import ( "encoding/hex" "encoding/json" "fmt" + "os" "strconv" "time" agentpb "github.com/pulumi/pulumi-kubernetes-operator/v2/agent/pkg/proto" autov1alpha1 "github.com/pulumi/pulumi-kubernetes-operator/v2/operator/api/auto/v1alpha1" + autov1alpha1webhook "github.com/pulumi/pulumi-kubernetes-operator/v2/operator/internal/webhook/auto/v1alpha1" "github.com/pulumi/pulumi-kubernetes-operator/v2/operator/version" appsv1 "k8s.io/api/apps/v1" corev1 "k8s.io/api/core/v1" @@ -83,6 +85,14 @@ func (r *WorkspaceReconciler) Reconcile(ctx context.Context, req ctrl.Request) ( return ctrl.Result{}, client.IgnoreNotFound(err) } + // apply defaults to the workspace spec + // future: use a mutating webhook to apply defaults + defaulter := autov1alpha1webhook.WorkspaceCustomDefaulter{} + err = defaulter.Default(ctx, w) + if err != nil { + return ctrl.Result{}, fmt.Errorf("failed to apply defaults: %w", err) + } + ready := meta.FindStatusCondition(w.Status.Conditions, WorkspaceConditionTypeReady) if ready == nil { ready = &metav1.Condition{ @@ -317,11 +327,11 @@ func (r *WorkspaceReconciler) SetupWithManager(mgr ctrl.Manager) error { } const ( - FieldManager = "pulumi-kubernetes-operator" - WorkspaceContainerName = "server" - WorkspaceShareVolumeName = "share" - WorkspaceShareMountPath = "/share" - WorkspaceGrpcPort = 50051 + FieldManager = "pulumi-kubernetes-operator" + WorkspacePulumiContainerName = "pulumi" + WorkspaceShareVolumeName = "share" + WorkspaceShareMountPath = "/share" + WorkspaceGrpcPort = 50051 ) func nameForStatefulSet(w *autov1alpha1.Workspace) string { @@ -346,17 +356,33 @@ func labelsForStatefulSet(w *autov1alpha1.Workspace) map[string]string { } func newStatefulSet(ctx context.Context, w *autov1alpha1.Workspace, source *sourceSpec) (*appsv1.StatefulSet, error) { - // TODO: get from environment - workspaceAgentImage := "pulumi/pulumi-kubernetes-operator:" + version.Version + image := os.Getenv("AGENT_IMAGE") + if image == "" { + image = "pulumi/pulumi-kubernetes-operator:" + version.Version + } labels := labelsForStatefulSet(w) command := []string{ - "/share/agent", "serve", + "/share/tini", "/share/agent", "--", "serve", "--workspace", "/share/workspace", "--skip-install", } + env := w.Spec.Env + + // limit the memory usage to the reserved amount + // https://github.com/pulumi/pulumi-kubernetes-operator/issues/698 + env = append(env, corev1.EnvVar{ + Name: "AGENT_MEMLIMIT", + ValueFrom: &corev1.EnvVarSource{ + ResourceFieldRef: &corev1.ResourceFieldSelector{ + ContainerName: WorkspacePulumiContainerName, + Resource: "requests.memory", + }, + }, + }) + statefulset := &appsv1.StatefulSet{ TypeMeta: metav1.TypeMeta{ Kind: "StatefulSet", @@ -390,7 +416,7 @@ func newStatefulSet(ctx context.Context, w *autov1alpha1.Workspace, source *sour InitContainers: []corev1.Container{ { Name: "bootstrap", - Image: workspaceAgentImage, + Image: image, ImagePullPolicy: corev1.PullIfNotPresent, VolumeMounts: []corev1.VolumeMount{ { @@ -398,13 +424,13 @@ func newStatefulSet(ctx context.Context, w *autov1alpha1.Workspace, source *sour MountPath: WorkspaceShareMountPath, }, }, - Command: []string{"cp", "/agent", "/share/agent"}, + Args: []string{"cp", "/agent", "/tini", "/share/"}, }, }, Containers: []corev1.Container{ { - Name: "pulumi", - Image: getDefaultSSImage(w.Spec.Image, w.Spec.SecurityProfile), + Name: WorkspacePulumiContainerName, + Image: w.Spec.Image, ImagePullPolicy: w.Spec.ImagePullPolicy, Resources: w.Spec.Resources, VolumeMounts: []corev1.VolumeMount{ @@ -419,9 +445,10 @@ func newStatefulSet(ctx context.Context, w *autov1alpha1.Workspace, source *sour ContainerPort: WorkspaceGrpcPort, }, }, - Env: w.Spec.Env, - EnvFrom: w.Spec.EnvFrom, - Command: command, + Env: env, + EnvFrom: w.Spec.EnvFrom, + Command: command, + WorkingDir: "/share/workspace", }, }, Volumes: []corev1.Volume{ @@ -498,7 +525,7 @@ ln -s /share/source/$GIT_DIR /share/workspace container := corev1.Container{ Name: "fetch", - Image: workspaceAgentImage, + Image: image, ImagePullPolicy: corev1.PullIfNotPresent, VolumeMounts: []corev1.VolumeMount{ { @@ -506,8 +533,8 @@ ln -s /share/source/$GIT_DIR /share/workspace MountPath: WorkspaceShareMountPath, }, }, - Env: env, - Command: []string{"sh", "-c", script}, + Env: env, + Args: []string{"sh", "-c", script}, } statefulset.Spec.Template.Spec.InitContainers = append(statefulset.Spec.Template.Spec.InitContainers, container) } @@ -519,7 +546,7 @@ ln -s /share/source/$FLUX_DIR /share/workspace ` container := corev1.Container{ Name: "fetch", - Image: workspaceAgentImage, + Image: image, ImagePullPolicy: corev1.PullIfNotPresent, VolumeMounts: []corev1.VolumeMount{ { @@ -541,7 +568,7 @@ ln -s /share/source/$FLUX_DIR /share/workspace Value: source.Flux.Dir, }, }, - Command: []string{"sh", "-c", script}, + Args: []string{"sh", "-c", script}, } statefulset.Spec.Template.Spec.InitContainers = append(statefulset.Spec.Template.Spec.InitContainers, container) } @@ -674,23 +701,3 @@ func mergePodTemplateSpec(_ context.Context, base *corev1.PodTemplateSpec, patch return patchResult, nil } - -// getDefaultSSImage returns the default image for the StatefulSet container based on the security profile. -// If the user had provided an image, then that image is returned instead. -func getDefaultSSImage(image string, securityProfile autov1alpha1.SecurityProfile) string { - if image != "" { - return image - } - - switch securityProfile { - case autov1alpha1.SecurityProfileRestricted: - return autov1alpha1.SecurityProfileRestrictedDefaultImage - case autov1alpha1.SecurityProfileBaseline: - return autov1alpha1.SecurityProfileBaselineDefaultImage - default: - // This should not happen, since the securityProfile has a default value. - // If for some reason it is empty, then we should default to the baseline image - // since we can't tell if the container can run in a restricted environment. - return autov1alpha1.SecurityProfileBaselineDefaultImage - } -} diff --git a/operator/internal/controller/auto/workspace_controller_test.go b/operator/internal/controller/auto/workspace_controller_test.go index 36229c95..88532e6e 100644 --- a/operator/internal/controller/auto/workspace_controller_test.go +++ b/operator/internal/controller/auto/workspace_controller_test.go @@ -199,7 +199,7 @@ var _ = Describe("Workspace Controller", func() { _, err := reconcileF(ctx) Expect(err).NotTo(HaveOccurred()) container := findContainer(ss.Spec.Template.Spec.Containers, "pulumi") - Expect(container.Env).To(Equal(obj.Spec.Env)) + Expect(container.Env).To(ContainElements(obj.Spec.Env)) }) }) }) @@ -215,16 +215,33 @@ var _ = Describe("Workspace Controller", func() { _, err := reconcileF(ctx) Expect(err).NotTo(HaveOccurred()) container := findContainer(ss.Spec.Template.Spec.Containers, "pulumi") - Expect(container.EnvFrom).To(Equal(obj.Spec.EnvFrom)) + Expect(container.EnvFrom).To(ContainElements(obj.Spec.EnvFrom)) }) }) }) Describe("spec.resources", func() { - When("resources are set", func() { + When("requests are not set", func() { + BeforeEach(func(ctx context.Context) { + obj.Spec.Resources = corev1.ResourceRequirements{} + }) + It("configures the resources of the pulumi container", func(ctx context.Context) { + _, err := reconcileF(ctx) + Expect(err).NotTo(HaveOccurred()) + container := findContainer(ss.Spec.Template.Spec.Containers, "pulumi") + Expect(container.Resources).To(Equal(corev1.ResourceRequirements{ + Requests: corev1.ResourceList{ + corev1.ResourceMemory: resource.MustParse("64Mi"), + corev1.ResourceCPU: resource.MustParse("100m"), + }, + })) + }) + }) + + When("requests are set", func() { BeforeEach(func(ctx context.Context) { obj.Spec.Resources = corev1.ResourceRequirements{ - Limits: corev1.ResourceList{ + Requests: corev1.ResourceList{ corev1.ResourceCPU: resource.MustParse("1"), corev1.ResourceMemory: resource.MustParse("1Gi"), }, diff --git a/operator/internal/webhook/auto/v1alpha1/workspace_webhook.go b/operator/internal/webhook/auto/v1alpha1/workspace_webhook.go new file mode 100644 index 00000000..7b66becc --- /dev/null +++ b/operator/internal/webhook/auto/v1alpha1/workspace_webhook.go @@ -0,0 +1,90 @@ +/* +Copyright 2024. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package v1alpha1 + +import ( + "context" + "fmt" + + corev1 "k8s.io/api/core/v1" + "k8s.io/apimachinery/pkg/api/resource" + "k8s.io/apimachinery/pkg/runtime" + "sigs.k8s.io/controller-runtime/pkg/webhook" + + autov1alpha1 "github.com/pulumi/pulumi-kubernetes-operator/v2/operator/api/auto/v1alpha1" +) + +const ( + // SecurityProfileBaselineDefaultImage is the default image used when the security profile is 'baseline'. + SecurityProfileBaselineDefaultImage = "pulumi/pulumi:latest" + // SecurityProfileRestrictedDefaultImage is the default image used when the security profile is 'restricted'. + SecurityProfileRestrictedDefaultImage = "pulumi/pulumi:latest-nonroot" +) + +// // SetupWorkspaceWebhookWithManager registers the webhook for Workspace in the manager. +// func SetupWorkspaceWebhookWithManager(mgr ctrl.Manager) error { +// return ctrl.NewWebhookManagedBy(mgr).For(&autov1alpha1.Workspace{}). +// WithDefaulter(&WorkspaceCustomDefaulter{}). +// Complete() +// } + +//// +kubebuilder:webhook:path=/mutate-auto-pulumi-com-v1alpha1-workspace,mutating=true,failurePolicy=fail,sideEffects=None,groups=auto.pulumi.com,resources=workspaces,verbs=create;update,versions=v1alpha1,name=mworkspace-v1alpha1.kb.io,admissionReviewVersions=v1 + +// WorkspaceCustomDefaulter struct is responsible for setting default values on the custom resource of the +// Kind Workspace when those are created or updated. +// +// NOTE: The +kubebuilder:object:generate=false marker prevents controller-gen from generating DeepCopy methods, +// as it is used only for temporary operations and does not need to be deeply copied. +type WorkspaceCustomDefaulter struct { + // TODO(user): Add more fields as needed for defaulting +} + +var _ webhook.CustomDefaulter = &WorkspaceCustomDefaulter{} + +// Default implements webhook.CustomDefaulter so a webhook will be registered for the Kind Workspace. +func (d *WorkspaceCustomDefaulter) Default(ctx context.Context, obj runtime.Object) error { + w, ok := obj.(*autov1alpha1.Workspace) + if !ok { + return fmt.Errorf("expected an Workspace object but got %T", obj) + } + + if w.Spec.SecurityProfile == "" { + w.Spec.SecurityProfile = autov1alpha1.SecurityProfileRestricted + } + + if w.Spec.Image == "" { + switch w.Spec.SecurityProfile { + case autov1alpha1.SecurityProfileRestricted: + w.Spec.Image = SecurityProfileRestrictedDefaultImage + case autov1alpha1.SecurityProfileBaseline: + w.Spec.Image = SecurityProfileBaselineDefaultImage + } + } + + // default resource requirements here are designed to provide a "burstable" workspace. + if w.Spec.Resources.Requests == nil { + w.Spec.Resources.Requests = corev1.ResourceList{} + } + if w.Spec.Resources.Requests.Memory().IsZero() { + w.Spec.Resources.Requests[corev1.ResourceMemory] = resource.MustParse("64Mi") + } + if w.Spec.Resources.Requests.Cpu().IsZero() { + w.Spec.Resources.Requests[corev1.ResourceCPU] = resource.MustParse("100m") + } + + return nil +}