diff --git a/.github/workflows/lint.yaml b/.github/workflows/lint.yaml index a4618ee..7e0c936 100644 --- a/.github/workflows/lint.yaml +++ b/.github/workflows/lint.yaml @@ -1,4 +1,4 @@ -name: golangci-lint +name: Lint on: pull_request: @@ -12,7 +12,7 @@ permissions: jobs: golangci: - name: lint + name: golangci runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 @@ -24,3 +24,15 @@ jobs: with: version: v1.60 args: --timeout=8m --verbose + + go-test: + name: go test + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - name: Setup Go + uses: actions/setup-go@v5 + with: + check-latest: true + - name: Display Go version + run: go test ./... \ No newline at end of file diff --git a/Makefile b/Makefile index d142c87..ea4e1ab 100644 --- a/Makefile +++ b/Makefile @@ -146,7 +146,19 @@ endif .PHONY: install install: manifests kustomize ## Install CRDs into the K8s cluster specified in ~/.kube/config. - $(KUSTOMIZE) build config/crd | $(KUBECTL) apply --namespace ${NAMESPACE} -f - + @echo "Building CRD..." + $(KUSTOMIZE) build config/crd > /tmp/temp-crd.yaml + @echo "Applying CRD..." + @if $(KUBECTL) get -f /tmp/temp-crd.yaml >/dev/null 2>&1; then \ + echo "CRD exists, replacing..."; \ + $(KUBECTL) replace --namespace ${NAMESPACE} -f /tmp/temp-crd.yaml; \ + else \ + echo "CRD does not exist, creating..."; \ + $(KUBECTL) create --namespace ${NAMESPACE} -f /tmp/temp-crd.yaml; \ + fi + @echo "Cleaning up..." + rm -f /tmp/temp-crd.yaml + @echo "Done." .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/api/v1/store.go b/api/v1/store.go index 6e3866b..c666c4b 100644 --- a/api/v1/store.go +++ b/api/v1/store.go @@ -1,6 +1,8 @@ package v1 import ( + "maps" + autoscalerv2 "k8s.io/api/autoscaling/v2" corev1 "k8s.io/api/core/v1" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" @@ -28,8 +30,15 @@ type StoreList struct { } type StoreSpec struct { - Database DatabaseSpec `json:"database"` - Container ContainerSpec `json:"container"` + Database DatabaseSpec `json:"database"` + + Container ContainerSpec `json:"container"` + AdminDeploymentContainer ContainerMergeSpec `json:"adminDeploymentContainer,omitempty"` + WorkerDeploymentContainer ContainerMergeSpec `json:"workerDeploymentContainer,omitempty"` + StorefrontDeploymentContainer ContainerMergeSpec `json:"storefrontDeploymentContainer,omitempty"` + SetupJobContainer ContainerMergeSpec `json:"setupJobContainer,omitempty"` + MigrationJobContainer ContainerMergeSpec `json:"migrationJobContainer,omitempty"` + Network NetworkSpec `json:"network,omitempty"` S3Storage S3Storage `json:"s3Storage,omitempty"` CDNURL string `json:"cdnURL"` @@ -138,9 +147,9 @@ type ContainerSpec struct { // ReadinessProbe corev1.Probe `json:"readinessProbe,omitempty"` // LivenessProbe corev1.Probe `json:"livenessProbe,omitempty"` - NodeSelector map[string]string `json:"nodeSelector,omitempty"` Annotations map[string]string `json:"annotations,omitempty"` Labels map[string]string `json:"labels,omitempty"` + NodeSelector map[string]string `json:"nodeSelector,omitempty"` TopologySpreadConstraints []corev1.TopologySpreadConstraint `json:"topologySpreadConstraints,omitempty"` Tolerations []corev1.Toleration `json:"tolerations,omitempty"` Affinity corev1.Affinity `json:"affinity,omitempty"` @@ -159,6 +168,30 @@ type ContainerSpec struct { ExtraEnvs []corev1.EnvVar `json:"extraEnvs,omitempty"` } +type ContainerMergeSpec struct { + // +kubebuilder:validation:MinLength=1 + Image string `json:"image,omitempty"` + ImagePullPolicy corev1.PullPolicy `json:"imagePullPolicy,omitempty"` + ImagePullSecrets []corev1.LocalObjectReference `json:"imagePullSecrets,omitempty"` + Volumes []corev1.Volume `json:"volumes,omitempty"` + VolumeMounts []corev1.VolumeMount `json:"volumeMounts,omitempty"` + RestartPolicy corev1.RestartPolicy `json:"restartPolicy,omitempty"` + SecurityContext *corev1.PodSecurityContext `json:"podSecurityContext,omitempty"` + ExtraContainers []corev1.Container `json:"extraContainers,omitempty"` + Replicas int32 `json:"replicas,omitempty"` + ProgressDeadlineSeconds int32 `json:"progressDeadlineSeconds,omitempty"` + + Annotations map[string]string `json:"annotations,omitempty"` + Labels map[string]string `json:"labels,omitempty"` + NodeSelector map[string]string `json:"nodeSelector,omitempty"` + TopologySpreadConstraints []corev1.TopologySpreadConstraint `json:"topologySpreadConstraints,omitempty"` + Tolerations []corev1.Toleration `json:"tolerations,omitempty"` + Affinity corev1.Affinity `json:"affinity,omitempty"` + + Resources corev1.ResourceRequirements `json:"resources,omitempty"` + ExtraEnvs []corev1.EnvVar `json:"extraEnvs,omitempty"` +} + type SessionCacheSpec struct { RedisSpec `json:",inline"` @@ -278,3 +311,57 @@ type SecretRef struct { func (s *Store) GetSecretName() string { return s.Spec.SecretName } + +func (c *ContainerSpec) Merge(from ContainerMergeSpec) { + if from.Image != "" { + c.Image = from.Image + } + if from.ImagePullPolicy != "" { + c.ImagePullPolicy = from.ImagePullPolicy + } + if from.Replicas != 0 { + c.Replicas = from.Replicas + } + if from.ProgressDeadlineSeconds != 0 { + c.ProgressDeadlineSeconds = from.ProgressDeadlineSeconds + } + if from.RestartPolicy != "" { + c.RestartPolicy = from.RestartPolicy + } + if from.ExtraEnvs != nil { + c.ExtraEnvs = from.ExtraEnvs + } + if from.VolumeMounts != nil { + c.VolumeMounts = from.VolumeMounts + } + if from.ImagePullSecrets != nil { + c.ImagePullSecrets = from.ImagePullSecrets + } + if from.Volumes != nil { + c.Volumes = from.Volumes + } + if from.Resources.Requests != nil { + c.Resources.Requests = from.Resources.Requests + } + if from.Resources.Limits != nil { + c.Resources.Limits = from.Resources.Limits + } + if from.ExtraContainers != nil { + c.ExtraContainers = from.ExtraContainers + } + if from.NodeSelector != nil { + c.NodeSelector = from.NodeSelector + } + if from.TopologySpreadConstraints != nil { + c.TopologySpreadConstraints = from.TopologySpreadConstraints + } + if from.Tolerations != nil { + c.Tolerations = from.Tolerations + } + if from.Annotations != nil { + maps.Copy(c.Annotations, from.Annotations) + } + if from.Labels != nil { + maps.Copy(c.Labels, from.Labels) + } +} diff --git a/api/v1/store_test.go b/api/v1/store_test.go new file mode 100644 index 0000000..bb49367 --- /dev/null +++ b/api/v1/store_test.go @@ -0,0 +1,46 @@ +package v1_test + +import ( + "testing" + + v1 "github.com/shopware/shopware-operator/api/v1" + "github.com/stretchr/testify/assert" +) + +func TestStoreContainer(t *testing.T) { + con := &v1.Store{ + Spec: v1.StoreSpec{ + Container: v1.ContainerSpec{ + Image: "FirstContainer", + Replicas: 2, + Labels: map[string]string{ + "test": "FirstContainer", + "test2": "FirstContainer", + }, + Annotations: map[string]string{ + "test": "FirstContainer", + "test2": "FirstContainer", + }, + }, + }, + } + + con2 := v1.ContainerMergeSpec{ + Image: "SecondContainer", + Labels: map[string]string{ + "test": "SecondContainer", + "test2": "SecondContainer", + }, + Annotations: map[string]string{ + "test": "SecondContainer", + }, + } + + con.Spec.Container.Merge(con2) + assert.Equal(t, "SecondContainer", con.Spec.Container.Image) + assert.Equal(t, int32(2), con.Spec.Container.Replicas) + assert.Equal(t, "SecondContainer", con.Spec.Container.Labels["test"]) + assert.Equal(t, "SecondContainer", con.Spec.Container.Labels["test2"]) + assert.Equal(t, "SecondContainer", con.Spec.Container.Annotations["test"]) + assert.Equal(t, "FirstContainer", con.Spec.Container.Annotations["test2"]) +} diff --git a/api/v1/zz_generated.deepcopy.go b/api/v1/zz_generated.deepcopy.go index a317734..e15b784 100644 --- a/api/v1/zz_generated.deepcopy.go +++ b/api/v1/zz_generated.deepcopy.go @@ -58,8 +58,13 @@ func (in *BlackfireSpec) DeepCopy() *BlackfireSpec { } // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. -func (in *ContainerSpec) DeepCopyInto(out *ContainerSpec) { +func (in *ContainerMergeSpec) DeepCopyInto(out *ContainerMergeSpec) { *out = *in + if in.ImagePullSecrets != nil { + in, out := &in.ImagePullSecrets, &out.ImagePullSecrets + *out = make([]corev1.LocalObjectReference, len(*in)) + copy(*out, *in) + } if in.Volumes != nil { in, out := &in.Volumes, &out.Volumes *out = make([]corev1.Volume, len(*in)) @@ -74,11 +79,6 @@ func (in *ContainerSpec) DeepCopyInto(out *ContainerSpec) { (*in)[i].DeepCopyInto(&(*out)[i]) } } - if in.ImagePullSecrets != nil { - in, out := &in.ImagePullSecrets, &out.ImagePullSecrets - *out = make([]corev1.LocalObjectReference, len(*in)) - copy(*out, *in) - } if in.SecurityContext != nil { in, out := &in.SecurityContext, &out.SecurityContext *out = new(corev1.PodSecurityContext) @@ -91,6 +91,20 @@ func (in *ContainerSpec) DeepCopyInto(out *ContainerSpec) { (*in)[i].DeepCopyInto(&(*out)[i]) } } + if in.Annotations != nil { + in, out := &in.Annotations, &out.Annotations + *out = make(map[string]string, len(*in)) + for key, val := range *in { + (*out)[key] = val + } + } + if in.Labels != nil { + in, out := &in.Labels, &out.Labels + *out = make(map[string]string, len(*in)) + for key, val := range *in { + (*out)[key] = val + } + } if in.NodeSelector != nil { in, out := &in.NodeSelector, &out.NodeSelector *out = make(map[string]string, len(*in)) @@ -98,6 +112,75 @@ func (in *ContainerSpec) DeepCopyInto(out *ContainerSpec) { (*out)[key] = val } } + if in.TopologySpreadConstraints != nil { + in, out := &in.TopologySpreadConstraints, &out.TopologySpreadConstraints + *out = make([]corev1.TopologySpreadConstraint, len(*in)) + for i := range *in { + (*in)[i].DeepCopyInto(&(*out)[i]) + } + } + if in.Tolerations != nil { + in, out := &in.Tolerations, &out.Tolerations + *out = make([]corev1.Toleration, len(*in)) + for i := range *in { + (*in)[i].DeepCopyInto(&(*out)[i]) + } + } + in.Affinity.DeepCopyInto(&out.Affinity) + in.Resources.DeepCopyInto(&out.Resources) + if in.ExtraEnvs != nil { + in, out := &in.ExtraEnvs, &out.ExtraEnvs + *out = make([]corev1.EnvVar, len(*in)) + for i := range *in { + (*in)[i].DeepCopyInto(&(*out)[i]) + } + } +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new ContainerMergeSpec. +func (in *ContainerMergeSpec) DeepCopy() *ContainerMergeSpec { + if in == nil { + return nil + } + out := new(ContainerMergeSpec) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *ContainerSpec) DeepCopyInto(out *ContainerSpec) { + *out = *in + if in.Volumes != nil { + in, out := &in.Volumes, &out.Volumes + *out = make([]corev1.Volume, len(*in)) + for i := range *in { + (*in)[i].DeepCopyInto(&(*out)[i]) + } + } + if in.VolumeMounts != nil { + in, out := &in.VolumeMounts, &out.VolumeMounts + *out = make([]corev1.VolumeMount, len(*in)) + for i := range *in { + (*in)[i].DeepCopyInto(&(*out)[i]) + } + } + if in.ImagePullSecrets != nil { + in, out := &in.ImagePullSecrets, &out.ImagePullSecrets + *out = make([]corev1.LocalObjectReference, len(*in)) + copy(*out, *in) + } + if in.SecurityContext != nil { + in, out := &in.SecurityContext, &out.SecurityContext + *out = new(corev1.PodSecurityContext) + (*in).DeepCopyInto(*out) + } + if in.ExtraContainers != nil { + in, out := &in.ExtraContainers, &out.ExtraContainers + *out = make([]corev1.Container, len(*in)) + for i := range *in { + (*in)[i].DeepCopyInto(&(*out)[i]) + } + } if in.Annotations != nil { in, out := &in.Annotations, &out.Annotations *out = make(map[string]string, len(*in)) @@ -112,6 +195,13 @@ func (in *ContainerSpec) DeepCopyInto(out *ContainerSpec) { (*out)[key] = val } } + if in.NodeSelector != nil { + in, out := &in.NodeSelector, &out.NodeSelector + *out = make(map[string]string, len(*in)) + for key, val := range *in { + (*out)[key] = val + } + } if in.TopologySpreadConstraints != nil { in, out := &in.TopologySpreadConstraints, &out.TopologySpreadConstraints *out = make([]corev1.TopologySpreadConstraint, len(*in)) @@ -165,6 +255,7 @@ func (in *Credentials) DeepCopy() *Credentials { // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. func (in *DatabaseSpec) DeepCopyInto(out *DatabaseSpec) { *out = *in + out.HostRef = in.HostRef out.PasswordSecretRef = in.PasswordSecretRef } @@ -435,6 +526,11 @@ func (in *StoreSpec) DeepCopyInto(out *StoreSpec) { *out = *in out.Database = in.Database in.Container.DeepCopyInto(&out.Container) + in.AdminDeploymentContainer.DeepCopyInto(&out.AdminDeploymentContainer) + in.WorkerDeploymentContainer.DeepCopyInto(&out.WorkerDeploymentContainer) + in.StorefrontDeploymentContainer.DeepCopyInto(&out.StorefrontDeploymentContainer) + in.SetupJobContainer.DeepCopyInto(&out.SetupJobContainer) + in.MigrationJobContainer.DeepCopyInto(&out.MigrationJobContainer) in.Network.DeepCopyInto(&out.Network) out.S3Storage = in.S3Storage out.Blackfire = in.Blackfire @@ -443,6 +539,7 @@ func (in *StoreSpec) DeepCopyInto(out *StoreSpec) { in.HorizontalPodAutoscaler.DeepCopyInto(&out.HorizontalPodAutoscaler) out.SessionCache = in.SessionCache out.AppCache = in.AppCache + out.Worker = in.Worker out.AdminCredentials = in.AdminCredentials out.SetupHook = in.SetupHook out.MigrationHook = in.MigrationHook @@ -479,3 +576,19 @@ func (in *StoreStatus) DeepCopy() *StoreStatus { in.DeepCopyInto(out) return out } + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *WorkerSpec) DeepCopyInto(out *WorkerSpec) { + *out = *in + out.RedisSpec = in.RedisSpec +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new WorkerSpec. +func (in *WorkerSpec) DeepCopy() *WorkerSpec { + if in == nil { + return nil + } + out := new(WorkerSpec) + in.DeepCopyInto(out) + return out +} diff --git a/internal/deployment/admin.go b/internal/deployment/admin.go index fb1e356..b691ca0 100644 --- a/internal/deployment/admin.go +++ b/internal/deployment/admin.go @@ -31,13 +31,19 @@ func GetAdminDeployment( return search, err } -func AdminDeployment(store *v1.Store) *appsv1.Deployment { +func AdminDeployment(st *v1.Store) *appsv1.Deployment { + store := st.DeepCopy() + appName := "shopware-admin" labels := map[string]string{ "app": appName, } maps.Copy(labels, util.GetDefaultLabels(store)) + // Merge Overwritten adminContainer fields into container fields + store.Spec.Container.Merge(store.Spec.AdminDeploymentContainer) + maps.Copy(labels, store.Spec.Container.Labels) + containers := append(store.Spec.Container.ExtraContainers, corev1.Container{ LivenessProbe: &corev1.Probe{ ProbeHandler: corev1.ProbeHandler{ diff --git a/internal/deployment/storefront.go b/internal/deployment/storefront.go index 3c012d6..4edcea0 100644 --- a/internal/deployment/storefront.go +++ b/internal/deployment/storefront.go @@ -33,13 +33,19 @@ func GetStorefrontDeployment( return search, err } -func StorefrontDeployment(store *v1.Store) *appsv1.Deployment { +func StorefrontDeployment(st *v1.Store) *appsv1.Deployment { + store := st.DeepCopy() + appName := "shopware-storefront" labels := map[string]string{ "app": appName, } maps.Copy(labels, util.GetDefaultLabels(store)) + // Merge Overwritten storefrontContainer fields into container fields + store.Spec.Container.Merge(store.Spec.StorefrontDeploymentContainer) + maps.Copy(labels, store.Spec.Container.Labels) + containers := append(store.Spec.Container.ExtraContainers, corev1.Container{ Name: DEPLOYMENT_STOREFRONT_CONTAINER_NAME, LivenessProbe: &corev1.Probe{ diff --git a/internal/deployment/worker.go b/internal/deployment/worker.go index 0a1d4f8..3ed9d72 100644 --- a/internal/deployment/worker.go +++ b/internal/deployment/worker.go @@ -31,13 +31,19 @@ func GetWorkerDeployment( return search, err } -func WorkerDeployment(store *v1.Store) *appsv1.Deployment { +func WorkerDeployment(st *v1.Store) *appsv1.Deployment { + store := st.DeepCopy() + appName := "shopware-worker" labels := map[string]string{ "app": appName, } maps.Copy(labels, util.GetDefaultLabels(store)) + // Merge Overwritten storefrontContainer fields into container fields + store.Spec.Container.Merge(store.Spec.WorkerDeploymentContainer) + maps.Copy(labels, store.Spec.Container.Labels) + containers := append(store.Spec.Container.ExtraContainers, corev1.Container{ Name: appName, Image: store.Spec.Container.Image, diff --git a/internal/job/migration.go b/internal/job/migration.go index f2fe039..1a04cf7 100644 --- a/internal/job/migration.go +++ b/internal/job/migration.go @@ -36,7 +36,9 @@ func GetMigrationJob( return search, err } -func MigrationJob(store *v1.Store) *batchv1.Job { +func MigrationJob(st *v1.Store) *batchv1.Job { + store := st.DeepCopy() + parallelism := int32(1) completions := int32(1) sharedProcessNamespace := true @@ -47,6 +49,10 @@ func MigrationJob(store *v1.Store) *batchv1.Job { maps.Copy(labels, util.GetDefaultLabels(store)) maps.Copy(labels, MigrationJobIdentifyer) + // Merge Overwritten jobContainer fields into container fields + store.Spec.Container.Merge(store.Spec.MigrationJobContainer) + maps.Copy(labels, store.Spec.Container.Labels) + // Write images to annotations because they are longer then 63 characters which // is the limit for labels annotations := map[string]string{ diff --git a/internal/job/setup.go b/internal/job/setup.go index ba5a592..9f69920 100644 --- a/internal/job/setup.go +++ b/internal/job/setup.go @@ -29,7 +29,9 @@ func GetSetupJob(ctx context.Context, client client.Client, store *v1.Store) (*b return search, err } -func SetupJob(store *v1.Store) *batchv1.Job { +func SetupJob(st *v1.Store) *batchv1.Job { + store := st.DeepCopy() + parallelism := int32(1) completions := int32(1) sharedProcessNamespace := true @@ -39,6 +41,10 @@ func SetupJob(store *v1.Store) *batchv1.Job { } maps.Copy(labels, util.GetDefaultLabels(store)) + // Merge Overwritten jobContainer fields into container fields + store.Spec.Container.Merge(store.Spec.SetupJobContainer) + maps.Copy(labels, store.Spec.Container.Labels) + envs := append(store.GetEnv(), corev1.EnvVar{ Name: "INSTALL_ADMIN_PASSWORD",