diff --git a/kubernetes-modules/k8s-operator/.gitignore b/kubernetes-modules/k8s-operator/.gitignore new file mode 100644 index 000000000000..9a6a0350f245 --- /dev/null +++ b/kubernetes-modules/k8s-operator/.gitignore @@ -0,0 +1,41 @@ +#Maven +target/ +pom.xml.tag +pom.xml.releaseBackup +pom.xml.versionsBackup +release.properties +.flattened-pom.xml + +# Eclipse +.project +.classpath +.settings/ +bin/ + +# IntelliJ +.idea +*.ipr +*.iml +*.iws + +# NetBeans +nb-configuration.xml + +# Visual Studio Code +.vscode +.factorypath + +# OSX +.DS_Store + +# Vim +*.swp +*.swo + +# patch +*.orig +*.rej + +# Local environment +.env + diff --git a/kubernetes-modules/k8s-operator/README.md b/kubernetes-modules/k8s-operator/README.md new file mode 100644 index 000000000000..215c7d8e18c5 --- /dev/null +++ b/kubernetes-modules/k8s-operator/README.md @@ -0,0 +1,4 @@ +# Dependency-Track operator + +This sample demonstrates how to create a simple operator using the Java Operator Framework. In our case, the operator will facilitate +the deployment of a Dependency-Track instance on a cluster. diff --git a/kubernetes-modules/k8s-operator/k8s/deptrack-controller.yaml b/kubernetes-modules/k8s-operator/k8s/deptrack-controller.yaml new file mode 100644 index 000000000000..7d5a020fdf63 --- /dev/null +++ b/kubernetes-modules/k8s-operator/k8s/deptrack-controller.yaml @@ -0,0 +1,88 @@ +apiVersion: v1 +kind: Namespace +metadata: + name: deptrack-operator + +--- +apiVersion: v1 +kind: ServiceAccount +metadata: + name: deptrack-operator + namespace: deptrack-operator + +--- +apiVersion: apps/v1 +kind: Deployment +metadata: + name: deptrack-operator + namespace: deptrack-operator +spec: + selector: + matchLabels: + app: deptrack-operator + template: + metadata: + labels: + app: deptrack-operator + spec: + serviceAccountName: deptrack-operator + containers: + - name: operator + image: deptrack-operator + imagePullPolicy: IfNotPresent + ports: + - containerPort: 8080 + readinessProbe: + httpGet: + path: /actuator/health/readiness + port: 8080 + initialDelaySeconds: 5 + livenessProbe: + httpGet: + path: /actuator/health/liveness + port: 8080 + initialDelaySeconds: 30 + +--- +apiVersion: rbac.authorization.k8s.io/v1 +kind: ClusterRoleBinding +metadata: + name: deptrack-operator-admin +subjects: + - kind: ServiceAccount + name: deptrack-operator + namespace: deptrack-operator +roleRef: + kind: ClusterRole + name: deptrack-operator + apiGroup: "" + +--- +apiVersion: rbac.authorization.k8s.io/v1 +kind: ClusterRole +metadata: + name: deptrack-operator +rules: + - apiGroups: + - "" + resources: + - deployments + - services + - ingresses + - configmaps + - secrets + verbs: + - '*' + - apiGroups: + - "apiextensions.k8s.io" + resources: + - customresourcedefinitions + verbs: + - '*' + - apiGroups: + - "com.baeldung" + resources: + - deptrackresources + - deptrackresources/status + verbs: + - '*' \ No newline at end of file diff --git a/kubernetes-modules/k8s-operator/k8s/test-resource.yaml b/kubernetes-modules/k8s-operator/k8s/test-resource.yaml new file mode 100644 index 000000000000..86154edfa40c --- /dev/null +++ b/kubernetes-modules/k8s-operator/k8s/test-resource.yaml @@ -0,0 +1,12 @@ +apiVersion: com.baeldung/v1 +kind: DeptrackResource +metadata: + namespace: test + name: deptrack1 + labels: + project: tutorials + annotations: + author: Philippe Sevestre + +spec: + ingressHostname: deptrack.172.31.42.16.nip.io diff --git a/kubernetes-modules/k8s-operator/pom.xml b/kubernetes-modules/k8s-operator/pom.xml new file mode 100644 index 000000000000..e3fedd6418d7 --- /dev/null +++ b/kubernetes-modules/k8s-operator/pom.xml @@ -0,0 +1,111 @@ + + + 4.0.0 + + + com.baeldung + parent-boot-3 + 0.0.1-SNAPSHOT + ./../../parent-boot-3 + + + k8s-operator + 0.1.0-SNAPSHOT + k8s-operator + jar + + + 17 + 17 + 4.6.0 + 6.9.2 + 1.77 + 2.0.9 + 5.4.0 + + + + + + + io.javaoperatorsdk + operator-framework-spring-boot-starter + ${operator-framework-spring-boot.version} + + + + io.javaoperatorsdk + operator-framework-spring-boot-starter-test + ${operator-framework-spring-boot.version} + test + + + org.apache.logging.log4j + log4j-slf4j2-impl + + + + + + org.springframework.boot + spring-boot-starter-webflux + + + + org.springframework.boot + spring-boot-starter-actuator + + + + org.projectlombok + lombok + true + + + + io.fabric8 + crd-generator-apt + ${fabric8-client.version} + provided + + + + org.bouncycastle + bcprov-jdk18on + ${bouncycastle.version} + + + + org.bouncycastle + bcpkix-jdk18on + ${bouncycastle.version} + + + + org.awaitility + awaitility + test + + + + + + + org.apache.maven.plugins + maven-compiler-plugin + + + org.springframework.boot + spring-boot-maven-plugin + + + deptrack-operator + + + + + + + \ No newline at end of file diff --git a/kubernetes-modules/k8s-operator/src/main/java/com/baeldung/operators/deptrack/Application.java b/kubernetes-modules/k8s-operator/src/main/java/com/baeldung/operators/deptrack/Application.java new file mode 100644 index 000000000000..3f72ce720487 --- /dev/null +++ b/kubernetes-modules/k8s-operator/src/main/java/com/baeldung/operators/deptrack/Application.java @@ -0,0 +1,18 @@ +package com.baeldung.operators.deptrack; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.boot.SpringApplication; +import org.springframework.boot.autoconfigure.SpringBootApplication; + + +@SpringBootApplication +public class Application { + + private static final Logger log = LoggerFactory.getLogger(Application.class); + + public static void main(String[] args) { + SpringApplication.run(Application.class,args); + } + +} diff --git a/kubernetes-modules/k8s-operator/src/main/java/com/baeldung/operators/deptrack/config/OperatorConfiguration.java b/kubernetes-modules/k8s-operator/src/main/java/com/baeldung/operators/deptrack/config/OperatorConfiguration.java new file mode 100644 index 000000000000..8c04dd802b25 --- /dev/null +++ b/kubernetes-modules/k8s-operator/src/main/java/com/baeldung/operators/deptrack/config/OperatorConfiguration.java @@ -0,0 +1,10 @@ +package com.baeldung.operators.deptrack.config; + +import org.springframework.context.annotation.Configuration; + +@Configuration +public class OperatorConfiguration { + + + +} diff --git a/kubernetes-modules/k8s-operator/src/main/java/com/baeldung/operators/deptrack/controller/deptrack/DeptrackOperatorReconciler.java b/kubernetes-modules/k8s-operator/src/main/java/com/baeldung/operators/deptrack/controller/deptrack/DeptrackOperatorReconciler.java new file mode 100644 index 000000000000..3adc7544eb71 --- /dev/null +++ b/kubernetes-modules/k8s-operator/src/main/java/com/baeldung/operators/deptrack/controller/deptrack/DeptrackOperatorReconciler.java @@ -0,0 +1,46 @@ +package com.baeldung.operators.deptrack.controller.deptrack; + +import org.springframework.context.ApplicationContext; +import org.springframework.stereotype.Component; + +import com.baeldung.operators.deptrack.resources.deptrack.DeptrackApiServerDeploymentResource; +import com.baeldung.operators.deptrack.resources.deptrack.DeptrackApiServerServiceResource; +import com.baeldung.operators.deptrack.resources.deptrack.DeptrackFrontendDeploymentResource; +import com.baeldung.operators.deptrack.resources.deptrack.DeptrackFrontendServiceResource; +import com.baeldung.operators.deptrack.resources.deptrack.DeptrackIngressResource; +import com.baeldung.operators.deptrack.resources.deptrack.DeptrackResource; + +import io.javaoperatorsdk.operator.api.reconciler.Context; +import io.javaoperatorsdk.operator.api.reconciler.ControllerConfiguration; +import io.javaoperatorsdk.operator.api.reconciler.Reconciler; +import io.javaoperatorsdk.operator.api.reconciler.UpdateControl; +import io.javaoperatorsdk.operator.api.reconciler.dependent.Dependent; +import jakarta.annotation.PostConstruct; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; + +@ControllerConfiguration(dependents = { + @Dependent(name = DeptrackApiServerDeploymentResource.COMPONENT, type = DeptrackApiServerDeploymentResource.class), + @Dependent(name = DeptrackFrontendDeploymentResource.COMPONENT, type = DeptrackFrontendDeploymentResource.class), + @Dependent(name = DeptrackApiServerServiceResource.COMPONENT, type = DeptrackApiServerServiceResource.class), + @Dependent(name = DeptrackFrontendServiceResource.COMPONENT, type = DeptrackFrontendServiceResource.class), + @Dependent(type = DeptrackIngressResource.class ) +}) +@Slf4j +@RequiredArgsConstructor +@Component +public class DeptrackOperatorReconciler implements Reconciler { + + private final ApplicationContext ctx; + + + @PostConstruct + void onPostConstruct() { + log.info("Reconciler created"); + } + + @Override + public UpdateControl reconcile(DeptrackResource resource, Context context) throws Exception { + return UpdateControl.noUpdate(); + } +} diff --git a/kubernetes-modules/k8s-operator/src/main/java/com/baeldung/operators/deptrack/resources/Constants.java b/kubernetes-modules/k8s-operator/src/main/java/com/baeldung/operators/deptrack/resources/Constants.java new file mode 100644 index 000000000000..1376e08ed572 --- /dev/null +++ b/kubernetes-modules/k8s-operator/src/main/java/com/baeldung/operators/deptrack/resources/Constants.java @@ -0,0 +1,9 @@ +package com.baeldung.operators.deptrack.resources; + +public interface Constants { + + String OPERATOR_NAME = "dependency-track-demo-operator"; + String DEFAULT_API_SERVER_IMAGE = "dependencytrack/apiserver"; + String DEFAULT_FRONTEND_IMAGE = "dependencytrack/frontend"; + +} diff --git a/kubernetes-modules/k8s-operator/src/main/java/com/baeldung/operators/deptrack/resources/deptrack/BuilderHelper.java b/kubernetes-modules/k8s-operator/src/main/java/com/baeldung/operators/deptrack/resources/deptrack/BuilderHelper.java new file mode 100644 index 000000000000..754b6b1454cd --- /dev/null +++ b/kubernetes-modules/k8s-operator/src/main/java/com/baeldung/operators/deptrack/resources/deptrack/BuilderHelper.java @@ -0,0 +1,53 @@ +package com.baeldung.operators.deptrack.resources.deptrack; + +import java.io.IOException; +import java.io.InputStream; +import java.util.List; + +import com.baeldung.operators.deptrack.resources.Constants; +import com.fasterxml.jackson.databind.ObjectMapper; +import com.fasterxml.jackson.dataformat.yaml.YAMLFactory; + +import io.fabric8.kubernetes.api.model.ManagedFieldsEntry; +import io.fabric8.kubernetes.api.model.ObjectMetaBuilder; +import io.fabric8.kubernetes.client.CustomResource; + +public final class BuilderHelper { + private static ObjectMapper om; + static { + om = new ObjectMapper(new YAMLFactory()); + } + + private BuilderHelper(){} + + public static > ObjectMetaBuilder fromPrimary(T primary, String component) { + return new ObjectMetaBuilder() + .withNamespace(primary.getMetadata().getNamespace()) + .withManagedFields((List)null) + .addToLabels("component", component) + .addToLabels("name", primary.getMetadata().getName()) + .withName(primary.getMetadata().getName() + "-" + component) + .addToLabels("ManagedBy", Constants.OPERATOR_NAME); + } + + public static T loadTemplate(Class clazz, String resource) { + + ClassLoader cl = Thread.currentThread().getContextClassLoader(); + if ( cl == null ) { + cl = BuilderHelper.class.getClassLoader(); + } + + try (InputStream is = cl.getResourceAsStream(resource)){ + return loadTemplate(clazz, is); + } + catch(IOException ioe) { + throw new RuntimeException("Unable to load classpath resource '" + resource + "': " + ioe.getMessage()); + } + + + } + + public static T loadTemplate(Class clazz, InputStream is) throws IOException{ + return om.readValue(is, clazz); + } +} diff --git a/kubernetes-modules/k8s-operator/src/main/java/com/baeldung/operators/deptrack/resources/deptrack/DeptrackApiServerDeploymentResource.java b/kubernetes-modules/k8s-operator/src/main/java/com/baeldung/operators/deptrack/resources/deptrack/DeptrackApiServerDeploymentResource.java new file mode 100644 index 000000000000..ec4b63369019 --- /dev/null +++ b/kubernetes-modules/k8s-operator/src/main/java/com/baeldung/operators/deptrack/resources/deptrack/DeptrackApiServerDeploymentResource.java @@ -0,0 +1,102 @@ +package com.baeldung.operators.deptrack.resources.deptrack; + +import static com.baeldung.operators.deptrack.resources.deptrack.BuilderHelper.fromPrimary; + +import java.util.Map; + +import org.springframework.util.StringUtils; + +import com.baeldung.operators.deptrack.resources.Constants; + +import io.fabric8.kubernetes.api.model.LabelSelector; +import io.fabric8.kubernetes.api.model.LabelSelectorBuilder; +import io.fabric8.kubernetes.api.model.ObjectMeta; +import io.fabric8.kubernetes.api.model.PodSpec; +import io.fabric8.kubernetes.api.model.PodSpecBuilder; +import io.fabric8.kubernetes.api.model.PodTemplateSpec; +import io.fabric8.kubernetes.api.model.PodTemplateSpecBuilder; +import io.fabric8.kubernetes.api.model.apps.Deployment; +import io.fabric8.kubernetes.api.model.apps.DeploymentBuilder; +import io.fabric8.kubernetes.api.model.apps.DeploymentSpec; +import io.fabric8.kubernetes.api.model.apps.DeploymentSpecBuilder; +import io.javaoperatorsdk.operator.api.reconciler.Context; +import io.javaoperatorsdk.operator.api.reconciler.ResourceIDMatcherDiscriminator; +import io.javaoperatorsdk.operator.processing.dependent.kubernetes.CRUDKubernetesDependentResource; +import io.javaoperatorsdk.operator.processing.dependent.kubernetes.KubernetesDependent; +import io.javaoperatorsdk.operator.processing.event.ResourceID; + +@KubernetesDependent( resourceDiscriminator = DeptrackApiServerDeploymentResource.Discriminator.class) +public class DeptrackApiServerDeploymentResource extends CRUDKubernetesDependentResource { + + public static final String COMPONENT = "api-server"; + + private Deployment template; + public DeptrackApiServerDeploymentResource() { + super(Deployment.class); + this.template = BuilderHelper.loadTemplate(Deployment.class, "templates/api-server-deployment.yaml"); + } + + @Override + protected Deployment desired(DeptrackResource primary, Context context) { + + ObjectMeta meta = fromPrimary(primary,COMPONENT) + .build(); + + return new DeploymentBuilder(template) + .withMetadata(meta) + .withSpec(buildSpec(primary, meta)) + .build(); + } + + private DeploymentSpec buildSpec(DeptrackResource primary, ObjectMeta primaryMeta) { + + return new DeploymentSpecBuilder() + .withSelector(buildSelector(primaryMeta.getLabels())) + .withReplicas(1) // Dependenty track does not support multiple pods (yet) + .withTemplate(buildPodTemplate(primary,primaryMeta)) + .build(); + } + + private LabelSelector buildSelector(Map labels) { + return new LabelSelectorBuilder() + .addToMatchLabels(labels) + .build(); + } + + private PodTemplateSpec buildPodTemplate(DeptrackResource primary, ObjectMeta primaryMeta) { + + return new PodTemplateSpecBuilder() + .withMetadata(primaryMeta) + .withSpec(buildPodSpec(primary)) + .build(); + } + + private PodSpec buildPodSpec(DeptrackResource primary) { + + // Check for version override + String imageVersion = StringUtils.hasText(primary.getSpec().getApiServerVersion())? + ":" + primary.getSpec().getApiServerVersion().trim():""; + + // Check for image override + String imageName = StringUtils.hasText(primary.getSpec().getApiServerImage())? + primary.getSpec().getApiServerImage().trim(): Constants.DEFAULT_API_SERVER_IMAGE; + + //@formatter:off + return new PodSpecBuilder(template.getSpec().getTemplate().getSpec()) + .editContainer(0) // Assumes we have a single container + .withImage(imageName + imageVersion) + .and() + .build(); + //@formatter:on + } + + + static class Discriminator extends ResourceIDMatcherDiscriminator { + public Discriminator() { + super(COMPONENT, (p) -> new ResourceID(p.getMetadata() + .getName() + "-" + COMPONENT, p.getMetadata() + .getNamespace())); + } + } + +} diff --git a/kubernetes-modules/k8s-operator/src/main/java/com/baeldung/operators/deptrack/resources/deptrack/DeptrackApiServerServiceResource.java b/kubernetes-modules/k8s-operator/src/main/java/com/baeldung/operators/deptrack/resources/deptrack/DeptrackApiServerServiceResource.java new file mode 100644 index 000000000000..95d5e3468b6a --- /dev/null +++ b/kubernetes-modules/k8s-operator/src/main/java/com/baeldung/operators/deptrack/resources/deptrack/DeptrackApiServerServiceResource.java @@ -0,0 +1,52 @@ +package com.baeldung.operators.deptrack.resources.deptrack; + +import static com.baeldung.operators.deptrack.resources.deptrack.BuilderHelper.fromPrimary; + +import java.util.HashMap; +import java.util.Map; + +import io.fabric8.kubernetes.api.model.ObjectMeta; +import io.fabric8.kubernetes.api.model.Service; +import io.fabric8.kubernetes.api.model.ServiceBuilder; +import io.javaoperatorsdk.operator.api.reconciler.Context; +import io.javaoperatorsdk.operator.api.reconciler.ResourceIDMatcherDiscriminator; +import io.javaoperatorsdk.operator.processing.dependent.kubernetes.CRUDKubernetesDependentResource; +import io.javaoperatorsdk.operator.processing.dependent.kubernetes.KubernetesDependent; +import io.javaoperatorsdk.operator.processing.event.ResourceID; + +@KubernetesDependent(resourceDiscriminator = DeptrackApiServerServiceResource.Discriminator.class) +public class DeptrackApiServerServiceResource extends CRUDKubernetesDependentResource { + + public static final String COMPONENT = "api-server-service"; + + private Service template; + public DeptrackApiServerServiceResource() { + super(Service.class); + this.template = BuilderHelper.loadTemplate(Service.class, "templates/api-server-service.yaml"); + } + + @Override + protected Service desired(DeptrackResource primary, Context context) { + + ObjectMeta meta = fromPrimary(primary,COMPONENT) + .build(); + + Map selector = new HashMap<>(meta.getLabels()); + selector.put("component", DeptrackApiServerDeploymentResource.COMPONENT); + + return new ServiceBuilder(template) + .withMetadata(meta) + .editSpec() + .withSelector(selector) + .endSpec() + .build(); + } + + + static class Discriminator extends ResourceIDMatcherDiscriminator { + public Discriminator() { + super(COMPONENT, (p) -> new ResourceID(p.getMetadata().getName() + "-" + COMPONENT,p.getMetadata().getNamespace())); + } + } + +} diff --git a/kubernetes-modules/k8s-operator/src/main/java/com/baeldung/operators/deptrack/resources/deptrack/DeptrackFrontendDeploymentResource.java b/kubernetes-modules/k8s-operator/src/main/java/com/baeldung/operators/deptrack/resources/deptrack/DeptrackFrontendDeploymentResource.java new file mode 100644 index 000000000000..ade29f940086 --- /dev/null +++ b/kubernetes-modules/k8s-operator/src/main/java/com/baeldung/operators/deptrack/resources/deptrack/DeptrackFrontendDeploymentResource.java @@ -0,0 +1,102 @@ +package com.baeldung.operators.deptrack.resources.deptrack; + +import static com.baeldung.operators.deptrack.resources.deptrack.BuilderHelper.fromPrimary; + +import java.util.Map; + +import org.springframework.util.StringUtils; + +import com.baeldung.operators.deptrack.resources.Constants; + +import io.fabric8.kubernetes.api.model.LabelSelector; +import io.fabric8.kubernetes.api.model.LabelSelectorBuilder; +import io.fabric8.kubernetes.api.model.ObjectMeta; +import io.fabric8.kubernetes.api.model.PodSpec; +import io.fabric8.kubernetes.api.model.PodSpecBuilder; +import io.fabric8.kubernetes.api.model.PodTemplateSpec; +import io.fabric8.kubernetes.api.model.PodTemplateSpecBuilder; +import io.fabric8.kubernetes.api.model.apps.Deployment; +import io.fabric8.kubernetes.api.model.apps.DeploymentBuilder; +import io.fabric8.kubernetes.api.model.apps.DeploymentSpec; +import io.fabric8.kubernetes.api.model.apps.DeploymentSpecBuilder; +import io.javaoperatorsdk.operator.api.reconciler.Context; +import io.javaoperatorsdk.operator.api.reconciler.ResourceIDMatcherDiscriminator; +import io.javaoperatorsdk.operator.processing.dependent.kubernetes.CRUDKubernetesDependentResource; +import io.javaoperatorsdk.operator.processing.dependent.kubernetes.KubernetesDependent; +import io.javaoperatorsdk.operator.processing.event.ResourceID; + +@KubernetesDependent(resourceDiscriminator = DeptrackFrontendDeploymentResource.Discriminator.class) +public class DeptrackFrontendDeploymentResource extends CRUDKubernetesDependentResource { + public static final String COMPONENT = "frontend"; + private Deployment template; + + public DeptrackFrontendDeploymentResource() { + super(Deployment.class); + this.template = BuilderHelper.loadTemplate(Deployment.class, "templates/frontend-deployment.yaml"); + } + + @Override + protected Deployment desired(DeptrackResource primary, Context context) { + + ObjectMeta meta = fromPrimary(primary,COMPONENT) + .build(); + + return new DeploymentBuilder(template).withMetadata(meta) + .withSpec(buildSpec(primary, meta)) + .build(); + } + + private DeploymentSpec buildSpec(DeptrackResource primary, ObjectMeta primaryMeta) { + + return new DeploymentSpecBuilder().withSelector(buildSelector(primaryMeta.getLabels())) + .withReplicas(1) // Dependency track does not support multiple pods (yet) + .withTemplate(buildPodTemplate(primary, primaryMeta)) + .build(); + } + + private LabelSelector buildSelector(Map labels) { + return new LabelSelectorBuilder().addToMatchLabels(labels) + .build(); + } + + private PodTemplateSpec buildPodTemplate(DeptrackResource primary, ObjectMeta primaryMeta) { + + return new PodTemplateSpecBuilder().withMetadata(primaryMeta) + .withSpec(buildPodSpec(primary)) + .build(); + } + + private PodSpec buildPodSpec(DeptrackResource primary) { + + // Check for version override + String imageVersion = StringUtils.hasText(primary.getSpec() + .getFrontendVersion()) ? ":" + primary.getSpec() + .getFrontendVersion() + .trim() : ""; + + // Check for image override + String imageName = StringUtils.hasText(primary.getSpec() + .getFrontendImage()) ? primary.getSpec() + .getFrontendImage() + .trim() : Constants.DEFAULT_FRONTEND_IMAGE; + + return new PodSpecBuilder(template.getSpec().getTemplate().getSpec()) + .editContainer(0) + .withImage(imageName + imageVersion) + .editFirstEnv() + .withName("API_BASE_URL") + .withValue("https://" + primary.getSpec().getIngressHostname()) + .endEnv() + .and() + .build(); + } + + static class Discriminator extends ResourceIDMatcherDiscriminator { + public Discriminator() { + super(COMPONENT, (p) -> new ResourceID(p.getMetadata() + .getName() + "-" + COMPONENT, p.getMetadata() + .getNamespace())); + } + } + +} diff --git a/kubernetes-modules/k8s-operator/src/main/java/com/baeldung/operators/deptrack/resources/deptrack/DeptrackFrontendServiceResource.java b/kubernetes-modules/k8s-operator/src/main/java/com/baeldung/operators/deptrack/resources/deptrack/DeptrackFrontendServiceResource.java new file mode 100644 index 000000000000..9b3d68397227 --- /dev/null +++ b/kubernetes-modules/k8s-operator/src/main/java/com/baeldung/operators/deptrack/resources/deptrack/DeptrackFrontendServiceResource.java @@ -0,0 +1,59 @@ +package com.baeldung.operators.deptrack.resources.deptrack; + +import static com.baeldung.operators.deptrack.resources.deptrack.BuilderHelper.fromPrimary; + +import java.util.HashMap; +import java.util.Map; + +import io.fabric8.kubernetes.api.model.ObjectMeta; +import io.fabric8.kubernetes.api.model.Service; +import io.fabric8.kubernetes.api.model.ServiceBuilder; +import io.javaoperatorsdk.operator.api.reconciler.Context; +import io.javaoperatorsdk.operator.api.reconciler.ResourceIDMatcherDiscriminator; +import io.javaoperatorsdk.operator.processing.dependent.kubernetes.CRUDKubernetesDependentResource; +import io.javaoperatorsdk.operator.processing.dependent.kubernetes.KubernetesDependent; +import io.javaoperatorsdk.operator.processing.event.ResourceID; + +@KubernetesDependent(resourceDiscriminator = DeptrackFrontendServiceResource.Discriminator.class) +public class DeptrackFrontendServiceResource extends CRUDKubernetesDependentResource { + + public static final String COMPONENT = "frontend-service"; + + private Service template; + public DeptrackFrontendServiceResource() { + super(Service.class); + this.template = BuilderHelper.loadTemplate(Service.class, "templates/frontend-service.yaml"); + } + + @Override + protected Service desired(DeptrackResource primary, Context context) { + + ObjectMeta meta = fromPrimary(primary,COMPONENT) + .build(); + + Map selector = new HashMap<>(meta.getLabels()); + selector.put("component", DeptrackFrontendDeploymentResource.COMPONENT); + + return new ServiceBuilder(template) + .withMetadata(meta) + .editSpec() + .withSelector(selector) + .endSpec() + .build(); + } + +// static class Discriminator implements ResourceDiscriminator { +// @Override +// public Optional distinguish(Class resource, DeptrackResource primary, Context context) { +// var ies = context.eventSourceRetriever().getResourceEventSourceFor(Service.class,COMPONENT); +// return ies.getSecondaryResource(primary); +// } +// } + + static class Discriminator extends ResourceIDMatcherDiscriminator { + public Discriminator() { + super(COMPONENT, (p) -> new ResourceID(p.getMetadata().getName() + "-" + COMPONENT,p.getMetadata().getNamespace())); + } + } + +} diff --git a/kubernetes-modules/k8s-operator/src/main/java/com/baeldung/operators/deptrack/resources/deptrack/DeptrackIngressResource.java b/kubernetes-modules/k8s-operator/src/main/java/com/baeldung/operators/deptrack/resources/deptrack/DeptrackIngressResource.java new file mode 100644 index 000000000000..27a6d21628a4 --- /dev/null +++ b/kubernetes-modules/k8s-operator/src/main/java/com/baeldung/operators/deptrack/resources/deptrack/DeptrackIngressResource.java @@ -0,0 +1,79 @@ +package com.baeldung.operators.deptrack.resources.deptrack; + +import static com.baeldung.operators.deptrack.resources.deptrack.BuilderHelper.fromPrimary; + +import io.fabric8.kubernetes.api.model.ObjectMeta; +import io.fabric8.kubernetes.api.model.networking.v1.HTTPIngressRuleValue; +import io.fabric8.kubernetes.api.model.networking.v1.HTTPIngressRuleValueBuilder; +import io.fabric8.kubernetes.api.model.networking.v1.Ingress; +import io.fabric8.kubernetes.api.model.networking.v1.IngressBuilder; +import io.javaoperatorsdk.operator.api.reconciler.Context; +import io.javaoperatorsdk.operator.processing.dependent.kubernetes.CRUDKubernetesDependentResource; +import io.javaoperatorsdk.operator.processing.dependent.kubernetes.KubernetesDependent; + +@KubernetesDependent +public class DeptrackIngressResource extends CRUDKubernetesDependentResource { + + private static final String COMPONENT = "ingress"; + private final Ingress template; + + public DeptrackIngressResource() { + super(Ingress.class); + this.template = BuilderHelper.loadTemplate(Ingress.class, "templates/ingress.yaml"); + } + + @Override + protected Ingress desired(DeptrackResource primary, Context context) { + + ObjectMeta meta = fromPrimary(primary,COMPONENT) + .build(); + + return new IngressBuilder(template) + .withMetadata(meta) + .editSpec() + .editDefaultBackend() + .editOrNewService() + .withName(primary.getFrontendServiceName()) + .endService() + .endDefaultBackend() + .editFirstRule() + .withHost(primary.getSpec().getIngressHostname()) + .withHttp(buildHttpRule(primary)) + .endRule() + .endSpec() + .build(); + + } + + private HTTPIngressRuleValue buildHttpRule(DeptrackResource primary) { + + return new HTTPIngressRuleValueBuilder() + // Backend route + .addNewPath() + .withPath("/api") + .withPathType("Prefix") + .withNewBackend() + .withNewService() + .withName(primary.getApiServerServiceName()) + .withNewPort() + .withName("http") + .endPort() + .endService() + .endBackend() + .endPath() + // Frontend route + .addNewPath() + .withPath("/") + .withPathType("Prefix") + .withNewBackend() + .withNewService() + .withName(primary.getFrontendServiceName()) + .withNewPort() + .withName("http") + .endPort() + .endService() + .endBackend() + .endPath() + .build(); + } +} diff --git a/kubernetes-modules/k8s-operator/src/main/java/com/baeldung/operators/deptrack/resources/deptrack/DeptrackResource.java b/kubernetes-modules/k8s-operator/src/main/java/com/baeldung/operators/deptrack/resources/deptrack/DeptrackResource.java new file mode 100644 index 000000000000..4f8fc4330eae --- /dev/null +++ b/kubernetes-modules/k8s-operator/src/main/java/com/baeldung/operators/deptrack/resources/deptrack/DeptrackResource.java @@ -0,0 +1,23 @@ +package com.baeldung.operators.deptrack.resources.deptrack; + +import com.fasterxml.jackson.annotation.JsonIgnore; + +import io.fabric8.kubernetes.api.model.Namespaced; +import io.fabric8.kubernetes.client.CustomResource; +import io.fabric8.kubernetes.model.annotation.Group; +import io.fabric8.kubernetes.model.annotation.Version; + +@Group("com.baeldung") +@Version("v1") +public class DeptrackResource extends CustomResource implements Namespaced { + + @JsonIgnore + public String getFrontendServiceName() { + return this.getMetadata().getName() + "-" + DeptrackFrontendServiceResource.COMPONENT; + } + + @JsonIgnore + public String getApiServerServiceName() { + return this.getMetadata().getName() + "-" + DeptrackApiServerServiceResource.COMPONENT; + } +} diff --git a/kubernetes-modules/k8s-operator/src/main/java/com/baeldung/operators/deptrack/resources/deptrack/DeptrackSpec.java b/kubernetes-modules/k8s-operator/src/main/java/com/baeldung/operators/deptrack/resources/deptrack/DeptrackSpec.java new file mode 100644 index 000000000000..54201d53f71a --- /dev/null +++ b/kubernetes-modules/k8s-operator/src/main/java/com/baeldung/operators/deptrack/resources/deptrack/DeptrackSpec.java @@ -0,0 +1,32 @@ +package com.baeldung.operators.deptrack.resources.deptrack; + +import java.util.Map; + +import lombok.Data; + +@Data +public class DeptrackSpec { + + // Images + private String apiServerImage = "dependencytrack/apiserver"; + private String apiServerVersion = ""; + + private String frontendImage = "dependencytrack/frontend"; + private String frontendVersion = ""; + + // PVC settings: NOT IMPLEMENTED + private String pvcClass = ""; // Use default storage class + private String pvcSize = "10Gi"; + + + // Database settings: NOT IMPLEMENTED + private String dbUrl; + private String dbDriver = "org.postgresql.Driver"; + private String dbSecret; + + + // Ingress settings + private String ingressHostname; + private Map ingressAnnotations; + +} diff --git a/kubernetes-modules/k8s-operator/src/main/java/com/baeldung/operators/deptrack/resources/deptrack/DeptrackStatus.java b/kubernetes-modules/k8s-operator/src/main/java/com/baeldung/operators/deptrack/resources/deptrack/DeptrackStatus.java new file mode 100644 index 000000000000..ee0d489cf7aa --- /dev/null +++ b/kubernetes-modules/k8s-operator/src/main/java/com/baeldung/operators/deptrack/resources/deptrack/DeptrackStatus.java @@ -0,0 +1,6 @@ +package com.baeldung.operators.deptrack.resources.deptrack; + +import io.javaoperatorsdk.operator.api.ObservedGenerationAwareStatus; + +public class DeptrackStatus extends ObservedGenerationAwareStatus { +} diff --git a/kubernetes-modules/k8s-operator/src/main/logback.xml b/kubernetes-modules/k8s-operator/src/main/logback.xml new file mode 100644 index 000000000000..7d900d8ea884 --- /dev/null +++ b/kubernetes-modules/k8s-operator/src/main/logback.xml @@ -0,0 +1,13 @@ + + + + + %d{HH:mm:ss.SSS} [%thread] %-5level %logger{36} - %msg%n + + + + + + + + \ No newline at end of file diff --git a/kubernetes-modules/k8s-operator/src/main/resources/application.properties b/kubernetes-modules/k8s-operator/src/main/resources/application.properties new file mode 100644 index 000000000000..3e91d731479a --- /dev/null +++ b/kubernetes-modules/k8s-operator/src/main/resources/application.properties @@ -0,0 +1 @@ +management.endpoint.health.probes.enabled=true \ No newline at end of file diff --git a/kubernetes-modules/k8s-operator/src/main/resources/templates/api-server-deployment.yaml b/kubernetes-modules/k8s-operator/src/main/resources/templates/api-server-deployment.yaml new file mode 100644 index 000000000000..61999ebc4a2f --- /dev/null +++ b/kubernetes-modules/k8s-operator/src/main/resources/templates/api-server-deployment.yaml @@ -0,0 +1,53 @@ +apiVersion: apps/v1 +kind: Deployment +metadata: + name: deptrack-api + namespace: deptrack +spec: + replicas: 1 + selector: + matchLabels: + ManagedBy: deptrack-operator + component: api-server + template: + metadata: + labels: + ManagedBy: deptrack-operator + component: api-server + spec: + containers: + - name: main + image: dependencytrack/apiserver:4.10.1 + ports: + - containerPort: 8080 + name: http + protocol: TCP + readinessProbe: + failureThreshold: 3 + httpGet: + path: / + port: http + scheme: HTTP + initialDelaySeconds: 60 + periodSeconds: 10 + successThreshold: 1 + timeoutSeconds: 2 + livenessProbe: + failureThreshold: 3 + httpGet: + path: /api/version + port: http + scheme: HTTP + initialDelaySeconds: 60 + periodSeconds: 10 + successThreshold: 1 + timeoutSeconds: 2 + resources: + limits: + cpu: "2" + memory: 8Gi + requests: + cpu: "1" + memory: 2Gi + restartPolicy: Always + diff --git a/kubernetes-modules/k8s-operator/src/main/resources/templates/api-server-pvc.yaml b/kubernetes-modules/k8s-operator/src/main/resources/templates/api-server-pvc.yaml new file mode 100644 index 000000000000..60a44e3259b2 --- /dev/null +++ b/kubernetes-modules/k8s-operator/src/main/resources/templates/api-server-pvc.yaml @@ -0,0 +1,11 @@ +apiVersion: v1 +kind: PersistentVolumeClaim +metadata: + name: deptrack-frontend + namespace: deptrack +spec: + accessModes: + - ReadWriteOnce + resources: + requests: + storage: 8Gi diff --git a/kubernetes-modules/k8s-operator/src/main/resources/templates/api-server-service.yaml b/kubernetes-modules/k8s-operator/src/main/resources/templates/api-server-service.yaml new file mode 100644 index 000000000000..160c84963efd --- /dev/null +++ b/kubernetes-modules/k8s-operator/src/main/resources/templates/api-server-service.yaml @@ -0,0 +1,19 @@ +apiVersion: v1 +kind: Service +metadata: + labels: + ManagedBy: deptrack-operator + component: api-server-service + name: deptrack-api-server + namespace: deptrack +spec: + ports: + - name: http + port: 8080 + protocol: TCP + targetPort: 8080 + selector: + ManagedBy: deptrack-operator + component: api-server + sessionAffinity: None + type: ClusterIP diff --git a/kubernetes-modules/k8s-operator/src/main/resources/templates/frontend-deployment.yaml b/kubernetes-modules/k8s-operator/src/main/resources/templates/frontend-deployment.yaml new file mode 100644 index 000000000000..4acdfc69bfd8 --- /dev/null +++ b/kubernetes-modules/k8s-operator/src/main/resources/templates/frontend-deployment.yaml @@ -0,0 +1,46 @@ +apiVersion: apps/v1 +kind: Deployment +metadata: + name: deptrack-frontend + namespace: deptrack +spec: + replicas: 1 + selector: + matchLabels: + ManagedBy: deptrack-operator + component: frontend + template: + metadata: + labels: + ManagedBy: deptrack-operator + component: frontend + spec: + containers: + - name: main + env: + - name: API_BASE_URL + value: https://example.com + image: dependencytrack/frontend + imagePullPolicy: Always + livenessProbe: + failureThreshold: 3 + httpGet: + path: / + port: http + scheme: HTTP + periodSeconds: 10 + successThreshold: 1 + timeoutSeconds: 1 + ports: + - containerPort: 8080 + name: http + protocol: TCP + readinessProbe: + failureThreshold: 3 + httpGet: + path: / + port: http + scheme: HTTP + periodSeconds: 10 + successThreshold: 1 + timeoutSeconds: 1 diff --git a/kubernetes-modules/k8s-operator/src/main/resources/templates/frontend-service.yaml b/kubernetes-modules/k8s-operator/src/main/resources/templates/frontend-service.yaml new file mode 100644 index 000000000000..d969afb9dda1 --- /dev/null +++ b/kubernetes-modules/k8s-operator/src/main/resources/templates/frontend-service.yaml @@ -0,0 +1,19 @@ +apiVersion: v1 +kind: Service +metadata: + labels: + ManagedBy: deptrack-operator + component: frontend-service + name: deptrack-frontend + namespace: deptrack +spec: + ports: + - name: http + port: 8080 + protocol: TCP + targetPort: 8080 + selector: + ManagedBy: deptrack-operator + component: frontend + sessionAffinity: None + type: ClusterIP diff --git a/kubernetes-modules/k8s-operator/src/main/resources/templates/ingress.yaml b/kubernetes-modules/k8s-operator/src/main/resources/templates/ingress.yaml new file mode 100644 index 000000000000..50871a7f8d92 --- /dev/null +++ b/kubernetes-modules/k8s-operator/src/main/resources/templates/ingress.yaml @@ -0,0 +1,33 @@ +apiVersion: networking.k8s.io/v1 +kind: Ingress +metadata: + annotations: + labels: + ManagedBy: terraform + component: ingress + name: deptrack-ingress + namespace: deptrack +spec: + defaultBackend: + service: + name: deptrack-frontend + port: + name: http + rules: + - host: example.com + http: + paths: + - backend: + service: + name: deptrack-api-server + port: + name: http + path: /api + pathType: Prefix + - backend: + service: + name: deptrack-frontend + port: + name: http + path: / + pathType: Prefix diff --git a/kubernetes-modules/k8s-operator/src/test/java/com/baeldung/operators/deptrack/ApplicationUnitTest.java b/kubernetes-modules/k8s-operator/src/test/java/com/baeldung/operators/deptrack/ApplicationUnitTest.java new file mode 100644 index 000000000000..8d4a11276529 --- /dev/null +++ b/kubernetes-modules/k8s-operator/src/test/java/com/baeldung/operators/deptrack/ApplicationUnitTest.java @@ -0,0 +1,30 @@ +package com.baeldung.operators.deptrack; + +import static org.assertj.core.api.Assertions.assertThat; + +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.context.SpringBootTest; + +import io.fabric8.kubernetes.client.KubernetesClient; +import io.javaoperatorsdk.operator.springboot.starter.test.EnableMockOperator; + +@SpringBootTest +@EnableMockOperator(crdPaths = "classpath:META-INF/fabric8/deptrackresources.com.baeldung-v1.yml") +class ApplicationUnitTest { + @Autowired + KubernetesClient client; + @Test + void whenContextLoaded_thenCrdRegistered() { + + assertThat( + client + .apiextensions() + .v1() + .customResourceDefinitions() + .withName("deptrackresources.com.baeldung") + .get()) + .isNotNull(); + } + +} \ No newline at end of file diff --git a/kubernetes-modules/pom.xml b/kubernetes-modules/pom.xml index 9bd623b4f44c..ec1e9468d709 100644 --- a/kubernetes-modules/pom.xml +++ b/kubernetes-modules/pom.xml @@ -18,6 +18,7 @@ k8s-admission-controller kubernetes-spring k8s-java-heap-dump + k8s-operator