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