├── .travis ├── .signing.asc.enc ├── settings.xml ├── .travis.e2e-oc.sh └── .travis.release.jars.sh ├── .mvn └── wrapper │ ├── maven-wrapper.jar │ ├── maven-wrapper.properties │ └── MavenWrapperDownloader.java ├── src └── main │ ├── resources │ ├── schema │ │ └── test.js │ ├── META-INF │ │ └── beans.xml │ └── log4j.properties │ ├── java │ └── io │ │ └── radanalytics │ │ └── operator │ │ ├── common │ │ ├── crd │ │ │ ├── InfoList.java │ │ │ ├── InfoClassDoneable.java │ │ │ ├── InfoClass.java │ │ │ ├── InfoStatus.java │ │ │ └── CrdDeployer.java │ │ ├── LogProducer.java │ │ ├── Operator.java │ │ ├── AnsiColors.java │ │ ├── JSONSchemaReader.java │ │ ├── EntityInfo.java │ │ ├── ProcessRunner.java │ │ ├── ConfigMapWatcher.java │ │ ├── CustomResourceWatcher.java │ │ ├── OperatorConfig.java │ │ ├── AbstractWatcher.java │ │ └── AbstractOperator.java │ │ ├── resource │ │ ├── ResourceHelper.java │ │ ├── LabelsHelper.java │ │ └── HasDataHelper.java │ │ └── SDKEntrypoint.java │ └── javadoc │ └── overview.html ├── .github ├── CODEOWNERS ├── ISSUE_TEMPLATE.md ├── PULL_REQUEST_TEMPLATE.md └── CONTRIBUTING.md ├── .travis.yml ├── annotator ├── src │ └── main │ │ └── java │ │ └── io │ │ └── radanalytics │ │ └── operator │ │ └── annotator │ │ └── RegisterForReflectionAnnotator.java └── pom.xml ├── Makefile ├── version-bump.sh ├── .gitignore ├── pom.xml ├── README.md ├── mvnw.cmd ├── mvnw └── LICENSE /.travis/.signing.asc.enc: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jvm-operators/abstract-operator/HEAD/.travis/.signing.asc.enc -------------------------------------------------------------------------------- /.mvn/wrapper/maven-wrapper.jar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jvm-operators/abstract-operator/HEAD/.mvn/wrapper/maven-wrapper.jar -------------------------------------------------------------------------------- /.mvn/wrapper/maven-wrapper.properties: -------------------------------------------------------------------------------- 1 | distributionUrl=https://repo.maven.apache.org/maven2/org/apache/maven/apache-maven/3.6.1/apache-maven-3.6.1-bin.zip -------------------------------------------------------------------------------- /src/main/resources/schema/test.js: -------------------------------------------------------------------------------- 1 | { 2 | "type":"object", 3 | "properties": { 4 | "foo": { 5 | "type": "string" 6 | }, 7 | "bar": { 8 | "type": "integer" 9 | }, 10 | "baz": { 11 | "type": "boolean" 12 | } 13 | } 14 | } -------------------------------------------------------------------------------- /.github/CODEOWNERS: -------------------------------------------------------------------------------- 1 | # Each line is a file pattern followed by one or more owners. 2 | # These owners will be the default owners for everything in 3 | # the repo. Unless a later match takes precedence. 4 | # 5 | # examples: 6 | # * @global-owner 7 | # *.js @js-owner 8 | # /docs/ @doctocat 9 | # *.go @org/team-name 10 | 11 | * @Jiri-Kremser 12 | -------------------------------------------------------------------------------- /src/main/resources/META-INF/beans.xml: -------------------------------------------------------------------------------- 1 | 2 | 8 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE.md: -------------------------------------------------------------------------------- 1 | ### Describe the problem: 2 | 3 | * With couple of sentences describe the problem and its context. 4 | 5 | #### Steps to reproduce: 6 | 7 | 1. 8 | 2. 9 | 3. 10 | 11 | #### Observed Results: 12 | 13 | * What happened? This could be a description, log output, etc. 14 | 15 | #### Expected Results: 16 | 17 | * What did you expect to happen? 18 | -------------------------------------------------------------------------------- /src/main/resources/log4j.properties: -------------------------------------------------------------------------------- 1 | # Root logger option 2 | log4j.rootLogger=INFO, stdout 3 | 4 | # Direct log messages to stdout 5 | log4j.appender.stdout=org.apache.log4j.ConsoleAppender 6 | log4j.appender.stdout.Target=System.out 7 | log4j.appender.stdout.layout=org.apache.log4j.PatternLayout 8 | log4j.appender.stdout.layout.ConversionPattern=%d{yyyy-MM-dd HH:mm:ss} %-5p %c{1}:%L - %m%n 9 | 10 | log4j.logger.org.reflections=FATAL -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: java 2 | sudo: true 3 | dist: trusty 4 | jdk: 5 | - oraclejdk8 6 | 7 | # skip the implicit install step 8 | install: true 9 | 10 | script: 11 | - make javadoc 12 | - make build-travis 13 | - make travis-e2e-use-case 14 | 15 | #deploy: 16 | # provider: script 17 | # script: make build-travis && ./mvnw -s ./.travis/settings.xml clean deploy 18 | after_success: 19 | - ./.travis/.travis.release.jars.sh 20 | -------------------------------------------------------------------------------- /src/main/java/io/radanalytics/operator/common/crd/InfoList.java: -------------------------------------------------------------------------------- 1 | package io.radanalytics.operator.common.crd; 2 | 3 | import com.fasterxml.jackson.databind.annotation.JsonDeserialize; 4 | import io.fabric8.kubernetes.client.CustomResourceList; 5 | import io.fabric8.kubernetes.internal.KubernetesDeserializer; 6 | 7 | @JsonDeserialize(using = KubernetesDeserializer.class) 8 | public class InfoList extends CustomResourceList> { 9 | } -------------------------------------------------------------------------------- /src/main/java/io/radanalytics/operator/common/crd/InfoClassDoneable.java: -------------------------------------------------------------------------------- 1 | package io.radanalytics.operator.common.crd; 2 | 3 | import io.fabric8.kubernetes.api.builder.Function; 4 | import io.fabric8.kubernetes.client.CustomResourceDoneable; 5 | 6 | 7 | public class InfoClassDoneable extends CustomResourceDoneable> { 8 | public InfoClassDoneable(InfoClass resource, Function function) { 9 | super(resource, function); 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /.travis/settings.xml: -------------------------------------------------------------------------------- 1 | 5 | 6 | 7 | ossrh 8 | ${env.SONATYPE_USER} 9 | ${env.SONATYPE_PASSWORD} 10 | 11 | 12 | 13 | 14 | -------------------------------------------------------------------------------- /src/main/java/io/radanalytics/operator/common/LogProducer.java: -------------------------------------------------------------------------------- 1 | package io.radanalytics.operator.common; 2 | 3 | import org.slf4j.Logger; 4 | import org.slf4j.LoggerFactory; 5 | 6 | import javax.enterprise.context.Dependent; 7 | import javax.enterprise.inject.Produces; 8 | import javax.enterprise.inject.spi.InjectionPoint; 9 | 10 | @Dependent 11 | class LogProducer { 12 | 13 | @Produces 14 | Logger createLogger(InjectionPoint injectionPoint) { 15 | return LoggerFactory.getLogger(injectionPoint.getMember().getDeclaringClass().getName()); 16 | } 17 | } -------------------------------------------------------------------------------- /annotator/src/main/java/io/radanalytics/operator/annotator/RegisterForReflectionAnnotator.java: -------------------------------------------------------------------------------- 1 | package io.radanalytics.operator.annotator; 2 | 3 | import com.fasterxml.jackson.databind.JsonNode; 4 | import com.sun.codemodel.JDefinedClass; 5 | import org.jsonschema2pojo.AbstractAnnotator; 6 | import io.quarkus.runtime.annotations.RegisterForReflection; 7 | 8 | public class RegisterForReflectionAnnotator extends AbstractAnnotator { 9 | 10 | @Override 11 | public void propertyOrder(JDefinedClass clazz, JsonNode propertiesNode) { 12 | super.propertyOrder(clazz, propertiesNode); 13 | clazz.annotate(RegisterForReflection.class); 14 | } 15 | } -------------------------------------------------------------------------------- /src/main/java/io/radanalytics/operator/common/crd/InfoClass.java: -------------------------------------------------------------------------------- 1 | package io.radanalytics.operator.common.crd; 2 | 3 | import io.fabric8.kubernetes.client.CustomResource; 4 | 5 | public class InfoClass extends CustomResource { 6 | private U spec; 7 | private InfoStatus status; 8 | 9 | public InfoClass() { 10 | this.status = new InfoStatus(); 11 | } 12 | 13 | public InfoStatus getStatus() { 14 | return this.status; 15 | } 16 | 17 | public void setStatus(InfoStatus status) { 18 | this.status = status; 19 | } 20 | 21 | public U getSpec() { 22 | return spec; 23 | } 24 | 25 | public void setSpec(U spec) { 26 | this.spec = spec; 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /.github/PULL_REQUEST_TEMPLATE.md: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | ## Description 5 | 6 | 7 | 8 | ## Related Issue 9 | 10 | 11 | 12 | ## Types of changes 13 | 14 | 15 | - [ ] Updated docs / Refactor code / Added a tests case / Automation (non-breaking change) 16 | - [ ] Bug fix (non-breaking change which fixes an issue) 17 | - [ ] New feature (non-breaking change which adds functionality) 18 | - [ ] Breaking change (fix or feature that would cause existing functionality to change) 19 | -------------------------------------------------------------------------------- /src/main/java/io/radanalytics/operator/common/Operator.java: -------------------------------------------------------------------------------- 1 | package io.radanalytics.operator.common; 2 | 3 | import java.lang.annotation.ElementType; 4 | import java.lang.annotation.Retention; 5 | import java.lang.annotation.RetentionPolicy; 6 | import java.lang.annotation.Target; 7 | 8 | @Target(ElementType.TYPE) 9 | @Retention(RetentionPolicy.RUNTIME) 10 | public @interface Operator { 11 | Class forKind(); 12 | String named() default ""; 13 | String prefix() default ""; 14 | String[] shortNames() default {}; 15 | String pluralName() default ""; 16 | boolean enabled() default true; 17 | boolean crd() default true; 18 | String[] additionalPrinterColumnNames() default {}; 19 | String[] additionalPrinterColumnPaths() default {}; 20 | String[] additionalPrinterColumnTypes() default {}; 21 | } 22 | -------------------------------------------------------------------------------- /.github/CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contributing 2 | We love your input! We want to make contributing to this project as easy and transparent as possible, whether it's: 3 | 4 | * Reporting a bug 5 | * Discussing the current state of the code 6 | * Submitting a fix 7 | * Proposing new features 8 | * Becoming a maintainer 9 | 10 | 11 | ## How to Contribute Code 12 | Pull requests are the best way to propose changes to the codebase. 13 | 14 | 1. Fork the repo and create your branch from master. 15 | 1. If you've added code that should be tested, add tests. 16 | 1. If you've changed APIs, update the documentation. 17 | 1. Ensure the test suite passes. 18 | 1. Make sure your code lints. 19 | 1. Issue that pull request! 20 | 21 | ## License 22 | By contributing, you agree that your contributions will be licensed under its Apache v2 License. 23 | 24 | Thank you! 25 | :octocat: 26 | -------------------------------------------------------------------------------- /src/main/java/io/radanalytics/operator/common/AnsiColors.java: -------------------------------------------------------------------------------- 1 | package io.radanalytics.operator.common; 2 | 3 | public class AnsiColors { 4 | 5 | // these shouldn't be used directly 6 | private static final String ANSI_R = "\u001B[31m"; 7 | private static final String ANSI_G = "\u001B[32m"; 8 | private static final String ANSI_Y = "\u001B[33m"; 9 | private static final String ANSI_RESET = "\u001B[0m"; 10 | 11 | // if empty, it's true 12 | public static final boolean COLORS = !"false".equals(System.getenv("COLORS")); 13 | 14 | public static String re() { 15 | return COLORS ? ANSI_R : ""; 16 | } 17 | 18 | public static String gr() { 19 | return COLORS ? ANSI_G : ""; 20 | } 21 | 22 | public static String ye() { 23 | return COLORS ? ANSI_Y : ""; 24 | } 25 | 26 | public static String xx() { 27 | return COLORS ? ANSI_RESET : ""; 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /src/main/java/io/radanalytics/operator/resource/ResourceHelper.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2018 3 | * License: Apache License 2.0 (see the file LICENSE or http://apache.org/licenses/LICENSE-2.0.html). 4 | */ 5 | package io.radanalytics.operator.resource; 6 | 7 | import io.fabric8.kubernetes.api.model.ConfigMap; 8 | 9 | import java.util.Optional; 10 | 11 | /** 12 | * A helper for parsing the top-lvl section inside the K8s resource 13 | */ 14 | public class ResourceHelper { 15 | 16 | /** 17 | * Returns the value of the {@code metadata.name} of the given {@code cm}. 18 | * 19 | * @param cm config map object 20 | * @return the name 21 | */ 22 | public static String name(ConfigMap cm) { 23 | return cm.getMetadata().getName(); 24 | } 25 | 26 | public static boolean isAKind(ConfigMap cm, String kind, String prefix) { 27 | return LabelsHelper.getKind(cm, prefix).map(k -> kind.equals(k)).orElse(false); 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | M ?= mvn 2 | 3 | .PHONY: build 4 | build: 5 | echo -e "travis_fold:start:jbuild\033[33;1mBuilding the Java code\033[0m" 6 | MAVEN_OPTS="-Djansi.passthrough=true -Dplexus.logger.type=ansi $(MAVEN_OPTS)" $(M) clean package -DskipTests 7 | echo -e "\ntravis_fold:end:jbuild\r" 8 | 9 | .PHONY: install-parent 10 | install-parent: 11 | git clone --depth=1 --branch master https://github.com/jvm-operators/operator-parent-pom.git && cd operator-parent-pom && MAVEN_OPTS="-Djansi.passthrough=true -Dplexus.logger.type=ansi $(MAVEN_OPTS)" $(M) clean install && cd - && rm -rf operator-parent-pom 12 | 13 | .PHONY: travis-e2e-use-case 14 | travis-e2e-use-case: 15 | ./.travis/.travis.e2e-oc.sh || echo -e "\n\nEnd-to-end scenario ended up with error" 16 | 17 | .PHONY: build-travis 18 | build-travis: install-parent build 19 | 20 | .PHONY: javadoc 21 | javadoc: 22 | echo -e "travis_fold:start:javadoc\033[33;1mGenerating Javadoc\033[0m" 23 | $(M) javadoc:javadoc 24 | echo -e "\ntravis_fold:end:javadoc\r" 25 | 26 | .PHONY: update-parent 27 | update-parent: 28 | $(M) -U versions:update-parent 29 | -------------------------------------------------------------------------------- /src/main/java/io/radanalytics/operator/common/JSONSchemaReader.java: -------------------------------------------------------------------------------- 1 | package io.radanalytics.operator.common; 2 | 3 | import com.fasterxml.jackson.databind.DeserializationFeature; 4 | import com.fasterxml.jackson.databind.ObjectMapper; 5 | import io.fabric8.kubernetes.api.model.apiextensions.JSONSchemaProps; 6 | 7 | import java.io.IOException; 8 | import java.net.URL; 9 | 10 | public class JSONSchemaReader { 11 | 12 | public static JSONSchemaProps readSchema(Class infoClass) { 13 | ObjectMapper mapper = new ObjectMapper(); 14 | mapper.configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false); 15 | char[] chars = infoClass.getSimpleName().toCharArray(); 16 | chars[0] = Character.toLowerCase(chars[0]); 17 | String urlJson = "/schema/" + new String(chars) + ".json"; 18 | String urlJS = "/schema/" + new String(chars) + ".js"; 19 | URL in = infoClass.getResource(urlJson); 20 | if (null == in) { 21 | // try also if .js file exists 22 | in = infoClass.getResource(urlJS); 23 | } 24 | if (null == in) { 25 | return null; 26 | } 27 | try { 28 | return mapper.readValue(in, JSONSchemaProps.class); 29 | } catch (IOException e) { 30 | e.printStackTrace(); 31 | return null; 32 | } 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /annotator/pom.xml: -------------------------------------------------------------------------------- 1 | 2 | 4 | 4.0.0 5 | 6 | io.radanalytics 7 | operator-parent-pom 8 | 0.3.22 9 | 10 | io.radanalytics 11 | abstract-operator-annotator 12 | 0.6.9-SNAPSHOT 13 | 14 | scm:git:git@github.com:jvm-operators/abstract-operator.git 15 | scm:git:git@github.com:jvm-operators/abstract-operator.git 16 | https://github.com/jvm-operators/abstract-operator 17 | 18 | 19 | 20 | UTF-8 21 | 22 | 23 | 24 | org.jsonschema2pojo 25 | jsonschema2pojo-core 26 | 0.4.0 27 | 28 | 29 | io.quarkus 30 | quarkus-core 31 | 32 | 33 | 34 | 35 | sonatype-releases 36 | https://oss.sonatype.org/content/repositories/releases 37 | 38 | 39 | 40 | -------------------------------------------------------------------------------- /src/main/java/io/radanalytics/operator/common/crd/InfoStatus.java: -------------------------------------------------------------------------------- 1 | package io.radanalytics.operator.common.crd; 2 | import io.fabric8.kubernetes.api.model.KubernetesResource; 3 | 4 | 5 | import java.text.DateFormat; 6 | import java.text.SimpleDateFormat; 7 | import java.util.Date; 8 | import java.util.TimeZone; 9 | 10 | public class InfoStatus implements KubernetesResource { 11 | 12 | private String state; 13 | private String lastTransitionTime; 14 | 15 | private static String toDateString(Date date) { 16 | DateFormat df = new SimpleDateFormat("yyyy-MM-dd'T'kk:mm:ss'Z'"); 17 | df.setTimeZone(TimeZone.getTimeZone( "GMT" )); 18 | return df.format(date); 19 | } 20 | 21 | public InfoStatus() { 22 | super(); 23 | this.state = "initial"; 24 | this.lastTransitionTime = toDateString(new Date()); 25 | } 26 | 27 | public InfoStatus(String state, Date dt) { 28 | super(); 29 | this.state = state; 30 | this.lastTransitionTime = toDateString(dt); 31 | } 32 | 33 | public void setState(String s) { 34 | this.state = s; 35 | } 36 | 37 | public String getState() { 38 | return this.state; 39 | } 40 | 41 | public void setLastTransitionTime(Date dt) { 42 | this.lastTransitionTime = toDateString(dt); 43 | } 44 | 45 | public String getLastTransitionTime() { 46 | return this.lastTransitionTime; 47 | } 48 | 49 | @Override 50 | public String toString() { 51 | return "InfoStatus{" + 52 | " state=" + state + 53 | " lastTransitionTime=" + lastTransitionTime + 54 | "}"; 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /src/main/java/io/radanalytics/operator/common/EntityInfo.java: -------------------------------------------------------------------------------- 1 | package io.radanalytics.operator.common; 2 | 3 | import java.util.Objects; 4 | 5 | /** 6 | * Simple abstract class that captures the information about the object we are interested in in the Kubernetes cluster. 7 | * Field called 'name' is the only compulsory information and it represents the name of the configmap. 8 | * 9 | * By extending this class and adding new fields to it, you can create a rich configuration object. The structure 10 | * of this object will be expected in the watched config maps and there are also some prepared method for YAML -> 11 | * 'T extends EntityInfo' conversions prepared in 12 | * {@link io.radanalytics.operator.resource.HasDataHelper#parseCM(Class, io.fabric8.kubernetes.api.model.ConfigMap)}. 13 | */ 14 | public abstract class EntityInfo { 15 | protected String name; 16 | protected String namespace; 17 | 18 | public void setName(String name) { 19 | this.name = name; 20 | } 21 | 22 | public String getName() { 23 | return name; 24 | } 25 | 26 | public void setNamespace(String namespace) { 27 | this.namespace = namespace; 28 | } 29 | 30 | public String getNamespace() { 31 | return namespace; 32 | } 33 | 34 | @Override 35 | public boolean equals(Object o) { 36 | if (this == o) return true; 37 | if (o == null || getClass() != o.getClass()) return false; 38 | EntityInfo that = (EntityInfo) o; 39 | return Objects.equals(name, that.name) && Objects.equals(namespace, that.namespace); 40 | } 41 | 42 | @Override 43 | public int hashCode() { 44 | return Objects.hash(name, namespace); 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /src/main/java/io/radanalytics/operator/resource/LabelsHelper.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2018 3 | * License: Apache License 2.0 (see the file LICENSE or http://apache.org/licenses/LICENSE-2.0.html). 4 | */ 5 | package io.radanalytics.operator.resource; 6 | 7 | import io.fabric8.kubernetes.api.model.HasMetadata; 8 | 9 | import java.util.Collections; 10 | import java.util.Map; 11 | import java.util.Optional; 12 | 13 | /** 14 | * A helper for parsing the {@code metadata.labels} section inside the K8s resource 15 | */ 16 | public class LabelsHelper { 17 | 18 | /** 19 | * The kind of a ConfigMap: 20 | *
    21 | *
  • {@code radanalytics.io/kind=cluster} 22 | * identifies a ConfigMap that is intended to be consumed by 23 | * the cluster operator.
  • 24 | *
  • {@code radanalytics.io/kind=app} 25 | * identifies a ConfigMap that is intended to be consumed 26 | * by the app operator.
  • 27 | *
  • {@code radanalytics.io/kind=notebook} 28 | * identifies a ConfigMap that is intended to be consumed 29 | * by the notebook operator.
  • 30 | *
31 | */ 32 | public static final String OPERATOR_KIND_LABEL = "kind"; 33 | 34 | public static final String OPERATOR_SEVICE_TYPE_LABEL = "service"; 35 | public static final String OPERATOR_RC_TYPE_LABEL = "rcType"; 36 | public static final String OPERATOR_POD_TYPE_LABEL = "podType"; 37 | public static final String OPERATOR_DEPLOYMENT_LABEL = "deployment"; 38 | 39 | public static final Optional getKind(HasMetadata resource, String prefix) { 40 | return Optional.ofNullable(resource) 41 | .map(r -> r.getMetadata()) 42 | .map(m -> m.getLabels()) 43 | .map(l -> l.get(prefix + OPERATOR_KIND_LABEL)); 44 | } 45 | 46 | public static Map forKind(String kind, String prefix) { 47 | return Collections.singletonMap(prefix + OPERATOR_KIND_LABEL, kind); 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /src/main/java/io/radanalytics/operator/common/ProcessRunner.java: -------------------------------------------------------------------------------- 1 | package io.radanalytics.operator.common; 2 | 3 | import org.slf4j.Logger; 4 | import org.slf4j.LoggerFactory; 5 | 6 | import java.io.BufferedReader; 7 | import java.io.IOException; 8 | import java.io.InputStreamReader; 9 | import java.util.Arrays; 10 | 11 | import static io.radanalytics.operator.common.AnsiColors.*; 12 | 13 | /** 14 | * Helper class that can be used from the concrete operators as the glue code for running the OS process. 15 | */ 16 | public class ProcessRunner { 17 | private static final Logger log = LoggerFactory.getLogger(AbstractOperator.class.getName()); 18 | 19 | public static void runPythonScript(String path) { 20 | runCommand("python3 " + path); 21 | } 22 | 23 | public static void runShellScript(String path) { 24 | runCommand(path); 25 | } 26 | 27 | 28 | public static void runCommand(String command) { 29 | try { 30 | String[] commands = new String[] {"sh", "-c", "\"" + command + "\""}; 31 | log.info("running: {}", Arrays.toString(commands)); 32 | Process p = Runtime.getRuntime().exec(commands); 33 | BufferedReader in = new BufferedReader(new InputStreamReader(p.getInputStream())); 34 | BufferedReader err = new BufferedReader(new InputStreamReader(p.getErrorStream())); 35 | String line; 36 | StringBuilder sb = new StringBuilder(); 37 | while ((line = in.readLine()) != null) { 38 | sb.append(line + "\n"); 39 | } 40 | String stdOutput = sb.toString(); 41 | if (!stdOutput.isEmpty()) { 42 | log.info("{}{}{}", gr(), stdOutput, xx()); 43 | } 44 | in.close(); 45 | 46 | sb = new StringBuilder(); 47 | while ((line = err.readLine()) != null) { 48 | sb.append(line + "\n"); 49 | } 50 | String errOutput = sb.toString(); 51 | if (!errOutput.isEmpty()) { 52 | log.error("{}{}{}", re(), stdOutput, xx()); 53 | } 54 | err.close(); 55 | } catch (IOException e) { 56 | log.error("Running '{}' failed with: {}", command, e.getMessage()); 57 | e.printStackTrace(); 58 | } 59 | } 60 | } 61 | -------------------------------------------------------------------------------- /version-bump.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | checkParams() { 4 | [[ $# -lt 1 ]] && printUsage && exit 1 5 | if [[ "$1" != "micro" ]] && [[ "$1" != "minor" ]] && [[ "$1" != "major" ]]; then 6 | printUsage 7 | exit 1 8 | fi 9 | } 10 | 11 | checkUntracked() { 12 | [[ -z $(git status -s) ]] || { 13 | echo "there are untracked files, commit them first" 14 | exit 1 15 | } 16 | } 17 | 18 | printUsage() { 19 | echo "usage: version-bump.sh " 20 | } 21 | 22 | gitFu() { 23 | [[ $# -lt 2 ]] && "usage: gitFu x.y.z x'.y'.z' " && exit 1 24 | old=$1 25 | new=$2 26 | mvn -U versions:set -DnewVersion=$new 27 | git add pom.xml 28 | set -x 29 | git commit -m "$3 version bump from $old to $new" 30 | set +x 31 | } 32 | 33 | main() { 34 | checkUntracked 35 | 36 | checkParams $@ 37 | PARAM=$1 38 | 39 | CURRENT=`mvn -U org.apache.maven.plugins:maven-help-plugin:2.1.1:evaluate -Dexpression=project.version|grep -Ev "(^\[|Download.+:)"` 40 | [[ ! "${CURRENT}" =~ ^[0-9]+\.[0-9]+\.[0-9]+(-SNAPSHOT)?$ ]] && echo "Can't find out the current version: $CURRENT" && exit 1 41 | VERSION=`echo $CURRENT | sed 's/-SNAPSHOT//g'` 42 | 43 | maj=`echo $VERSION | sed 's/^\([0-9]\+\)\..*$/\1/g'` 44 | min=`echo $VERSION | sed 's/^[0-9]\+\.\([0-9]\+\)\..*$/\1/g'` 45 | mic=`echo $VERSION | sed 's/.*\.\([0-9]\+\)$/\1/g'` 46 | 47 | echo "Current version: $CURRENT" 48 | echo "version: $VERSION" 49 | echo "major: $maj" 50 | echo "minor: $min" 51 | echo "micro: $mic" 52 | 53 | if [[ "$PARAM" = "micro" ]]; then 54 | echo "Updating micro version" 55 | elif [[ "$PARAM" = "minor" ]]; then 56 | echo "Updating minor version" 57 | ((min++)) 58 | mic=0 59 | elif [[ "$PARAM" = "major" ]]; then 60 | echo "Updating major version" 61 | ((maj++)) 62 | min=0 63 | mic=0 64 | else 65 | echo "Unrecognized param: $PARAM" 66 | printUsage && exit 1 67 | fi 68 | 69 | DESIRED="$maj.$min.$mic" 70 | gitFu "$CURRENT" "$DESIRED" "$PARAM" 71 | git tag -d "$DESIRED" || true 72 | git tag "$DESIRED" 73 | 74 | ((mic++)) 75 | DESIRED_NEW="$maj.$min.$mic-SNAPSHOT" 76 | 77 | echo "Desired new version: $DESIRED_NEW" 78 | gitFu "$DESIRED" "$DESIRED_NEW" "$PARAM" 79 | 80 | echo -e "if everything is ok, you may want to continue with: \n\n git push personal master $DESIRED\n\n" 81 | } 82 | 83 | main $@ 84 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | ###################### 2 | # Eclipse 3 | ###################### 4 | *.pydevproject 5 | .project 6 | .metadata 7 | tmp/ 8 | tmp/**/* 9 | *.tmp 10 | *.bak 11 | *.swp 12 | *~.nib 13 | local.properties 14 | .classpath 15 | .settings/ 16 | .loadpath 17 | .factorypath 18 | /src/main/resources/rebel.xml 19 | 20 | # External tool builders 21 | .externalToolBuilders/** 22 | 23 | # Locally stored "Eclipse launch configurations" 24 | *.launch 25 | 26 | # CDT-specific 27 | .cproject 28 | 29 | # PDT-specific 30 | .buildpath 31 | 32 | ###################### 33 | # Intellij 34 | ###################### 35 | .idea/ 36 | *.iml 37 | *.iws 38 | *.ipr 39 | *.ids 40 | *.orig 41 | classes/ 42 | 43 | ###################### 44 | # Visual Studio Code 45 | ###################### 46 | .vscode/ 47 | 48 | ###################### 49 | # Maven 50 | ###################### 51 | /log/ 52 | target/ 53 | 54 | ###################### 55 | # Gradle 56 | ###################### 57 | .gradle/ 58 | /build/ 59 | 60 | ###################### 61 | # Package Files 62 | ###################### 63 | *.jar 64 | *.war 65 | *.ear 66 | *.db 67 | 68 | ###################### 69 | # Windows 70 | ###################### 71 | # Windows image file caches 72 | Thumbs.db 73 | 74 | # Folder config file 75 | Desktop.ini 76 | 77 | ###################### 78 | # Mac OSX 79 | ###################### 80 | .DS_Store 81 | .svn 82 | 83 | # Thumbnails 84 | ._* 85 | 86 | # Files that might appear on external disk 87 | .Spotlight-V100 88 | .Trashes 89 | 90 | ###################### 91 | # Others 92 | ###################### 93 | *.class 94 | *.*~ 95 | *~ 96 | .merge_file* 97 | 98 | ###################### 99 | # Gradle Wrapper 100 | ###################### 101 | !gradle/wrapper/gradle-wrapper.jar 102 | 103 | ###################### 104 | # Maven Wrapper 105 | ###################### 106 | !.mvn/wrapper/maven-wrapper.jar 107 | 108 | ###################### 109 | # ESLint 110 | ###################### 111 | .eslintcache 112 | 113 | 114 | ########################## 115 | # Custom/Project Specific 116 | ########################## 117 | 118 | # generated manifests 119 | /k8s-spark-operator.yaml 120 | /openshift-spark-operator.yaml 121 | 122 | spark-operator 123 | 124 | # oc cluster up 125 | openshift.local.clusterup 126 | 127 | 128 | pom.xml.versionsBackup 129 | -------------------------------------------------------------------------------- /.travis/.travis.e2e-oc.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | DIR="${DIR:-$( cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd )}" 4 | BIN="oc" 5 | CRD="1" 6 | MANIFEST_SUFIX="" 7 | KIND="SparkCluster" 8 | VERSION="v3.9.0" 9 | 10 | run_tests() { 11 | testCreateCluster1 || errorLogs 12 | testScaleCluster || errorLogs 13 | testDeleteCluster || errorLogs 14 | sleep 5 15 | 16 | testMetricServer || errorLogs 17 | logs 18 | } 19 | 20 | prepare_operator() { 21 | set -ex 22 | echo -e "travis_fold:start:prepareOp\033[33;1mPreparing operator\033[0m" 23 | #LIBRARY_VERSION="$(mvn org.apache.maven.plugins:maven-help-plugin:2.1.1:evaluate -Dexpression=project.version|grep -Ev '(^\[|Download\w+:)')" 24 | LIBRARY_VERSION=$(cat pom.xml | grep -A 1 "abstract-operator" | grep version | sed 's;.*\([^<]\+\).*;\1;g') 25 | rm -rf ${DIR}/../spark-operator || true 26 | pushd ${DIR}/.. 27 | git clone --depth=100 --branch master --recurse-submodules https://github.com/radanalyticsio/spark-operator.git 28 | cd spark-operator 29 | 30 | # checkout the latest release 31 | GIT_TAG="$(git describe --abbrev=0 --tags)" 32 | [[ "${GIT_TAG}" =~ ^[0-9]+\.[0-9]+\.[0-9]+$ ]] && git checkout "${GIT_TAG}" 33 | 34 | # use the -SNAPSHOTed version of abstract operator 35 | sed -i'' "s;\(\)\([^<]\+\);\1${LIBRARY_VERSION};g" pom.xml 36 | make build 37 | source "./.travis/.travis.test-common.sh" 38 | echo "sourcing ./test/lib/init.sh" 39 | source "./test/lib/init.sh" 40 | echo -e "\ntravis_fold:end:prepareOp\r" 41 | source "./.travis/.travis.prepare.openshift.sh" 42 | popd 43 | 44 | set +ex 45 | } 46 | 47 | main() { 48 | set -x 49 | prepare_operator 50 | 51 | echo -e "travis_fold:start:e2e\033[33;1mSimple integration test\033[0m" 52 | DIR="${DIR}/../spark-operator/.travis" 53 | export total=6 54 | export testIndex=0 55 | tear_down 56 | os::util::environment::setup_time_vars 57 | os::test::junit::declare_suite_start "operator/tests" 58 | cluster_up 59 | testCreateOperator || { oc get events; oc get pods; exit 1; } 60 | export operator_pod=`oc get pod -l app.kubernetes.io/name=spark-operator -o='jsonpath="{.items[0].metadata.name}"' | sed 's/"//g'` 61 | run_tests 62 | os::test::junit::declare_suite_end 63 | tear_down 64 | echo -e "\ntravis_fold:end:e2e\r" 65 | set +x 66 | } 67 | 68 | main $@ -------------------------------------------------------------------------------- /.travis/.travis.release.jars.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | set -xe 4 | 5 | REPO="${REPO:-jvm-operators/abstract-operator}" 6 | 7 | [ "$TRAVIS_BRANCH" = "master" -a "$TRAVIS_PULL_REQUEST" = "false" ] && LATEST=1 8 | 9 | main() { 10 | if [[ "$LATEST" = "1" ]]; then 11 | echo "Pushing the -SNAPSHOT artifact to sonatype maven repo." 12 | releaseSnapshot 13 | javadoc 14 | elif [[ "${TRAVIS_TAG}" =~ ^[0-9]+\.[0-9]+\.[0-9]+$ ]]; then 15 | echo "Releasing the '${TRAVIS_TAG}' maven artifacts." 16 | # uncomment out if you want to make the travis do the releases (it wasn't reliable) 17 | release 18 | javadoc 19 | else 20 | echo "Not doing the Maven release, because the tag '${TRAVIS_TAG}' is not of form x.y.z" 21 | echo "and also it's not a build of the master branch" 22 | fi 23 | } 24 | 25 | releaseSnapshot() { 26 | make build-travis && mvn -s ./.travis/settings.xml clean deploy 27 | } 28 | 29 | release() { 30 | openssl aes-256-cbc -K ${encrypted_ea794cf5410d_key} -iv ${encrypted_ea794cf5410d_iv} -in ./.travis/.signing.asc.enc -out ./signing.asc -d 31 | gpg --fast-import ./signing.asc &> /dev/null 32 | mvn -s ./.travis/settings.xml clean deploy -DskipLocalStaging=true -P release 33 | ls ./target 34 | sleep 10 35 | local _repo_ids=`mvn -s ./.travis/settings.xml nexus-staging:rc-list | grep "ioradanalytics".*OPEN | cut -d' ' -f2 | tail -2 | sort -r` 36 | for _id in ${_repo_ids}; do 37 | mvn -s ./.travis/settings.xml nexus-staging:close nexus-staging:release -DstagingRepositoryId=${_id} || true 38 | done 39 | rm ./signing.asc 40 | } 41 | 42 | javadoc() { 43 | [ -z "$GH_TOKEN" ] && echo "GH_TOKEN not set, exiting.." && exit 0 44 | [[ "$LATEST" = "1" ]] && VERSION="latest" || VERSION=${TRAVIS_TAG} 45 | mvn -s ./.travis/settings.xml javadoc:javadoc 46 | cp -r ./target/site/apidocs/ /tmp/ 47 | switchBranch 48 | rm -rf ./docs/${VERSION} || true 49 | mv /tmp/apidocs ./docs/${VERSION} 50 | 51 | # release 52 | if [[ "$LATEST" != "1" ]]; then 53 | cd docs 54 | rm latest-released 55 | ln -s ${VERSION} latest-released 56 | cd - 57 | echo "
  • ${VERSION} - docs - maven
  • " >> ./index.html 58 | fi 59 | pushToScm ${VERSION} 60 | } 61 | 62 | switchBranch() { 63 | git fetch origin +refs/heads/gh-pages:refs/remotes/origin/gh-pages 64 | git remote set-branches --add origin gh-pages 65 | git checkout --track -b gh-pages origin/gh-pages 66 | } 67 | 68 | pushToScm() { 69 | VERSION=$1 70 | git add -A 71 | git commit -m "Docs for version ${VERSION}." 72 | set +x 73 | git remote add ad-hoc-origin https://Jiri-Kremser:${GH_TOKEN}@github.com/${REPO}.git 74 | set -x 75 | git push ad-hoc-origin gh-pages 76 | } 77 | 78 | main 79 | -------------------------------------------------------------------------------- /pom.xml: -------------------------------------------------------------------------------- 1 | 2 | 4 | 4.0.0 5 | 6 | io.radanalytics 7 | operator-parent-pom 8 | 0.3.25 9 | 10 | io.radanalytics 11 | abstract-operator 12 | 0.6.16-SNAPSHOT 13 | 14 | scm:git:git@github.com:jvm-operators/abstract-operator.git 15 | scm:git:git@github.com:jvm-operators/abstract-operator.git 16 | https://github.com/jvm-operators/abstract-operator 17 | 18 | 19 | UTF-8 20 | 21 | 22 | 23 | io.quarkus 24 | quarkus-kubernetes-client 25 | 26 | 27 | io.quarkus 28 | quarkus-arc 29 | 30 | 31 | io.prometheus 32 | simpleclient 33 | 34 | 35 | io.prometheus 36 | simpleclient_httpserver 37 | 38 | 39 | io.prometheus 40 | simpleclient_hotspot 41 | 42 | 43 | io.prometheus 44 | simpleclient_log4j 45 | 46 | 47 | org.slf4j 48 | slf4j-api 49 | 50 | 51 | org.slf4j 52 | slf4j-log4j12 53 | 54 | 55 | org.yaml 56 | snakeyaml 57 | 58 | 59 | com.jcabi 60 | jcabi-manifests 61 | 62 | 63 | commons-collections 64 | commons-collections 65 | 66 | 67 | commons-lang 68 | commons-lang 69 | 70 | 71 | com.google.guava 72 | guava 73 | 74 | 75 | junit 76 | junit 77 | test 78 | 79 | 80 | 81 | 82 | sonatype-releases 83 | https://oss.sonatype.org/content/repositories/releases 84 | 85 | 86 | 87 | 88 | 89 | org.codehaus.mojo 90 | buildnumber-maven-plugin 91 | 92 | 93 | org.jsonschema2pojo 94 | jsonschema2pojo-maven-plugin 95 | 96 | 97 | io.quarkus 98 | quarkus-maven-plugin 99 | 100 | 101 | 102 | 103 | -------------------------------------------------------------------------------- /src/main/java/io/radanalytics/operator/resource/HasDataHelper.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2018 3 | * License: Apache License 2.0 (see the file LICENSE or http://apache.org/licenses/LICENSE-2.0.html). 4 | */ 5 | package io.radanalytics.operator.resource; 6 | 7 | import io.fabric8.kubernetes.api.model.ConfigMap; 8 | import io.radanalytics.operator.common.EntityInfo; 9 | import org.slf4j.Logger; 10 | import org.slf4j.LoggerFactory; 11 | import org.yaml.snakeyaml.LoaderOptions; 12 | import org.yaml.snakeyaml.Yaml; 13 | import org.yaml.snakeyaml.constructor.Constructor; 14 | import org.yaml.snakeyaml.error.YAMLException; 15 | 16 | /** 17 | * A helper for parsing the data section inside the K8s resource (ConfigMap). 18 | * Type parameter T represents the concrete EntityInfo that captures the configuration obout the 19 | * objects in the clusters we are interested in, be it spark clusters, http servers, certificates, etc. 20 | * 21 | * One can create arbitrarily deep configurations by nesting the types in Class<T> and using 22 | * the Snake yaml or other library as for conversions between YAML and Java objects. 23 | */ 24 | public class HasDataHelper { 25 | private static final Logger log = LoggerFactory.getLogger(HasDataHelper.class.getName()); 26 | 27 | public static T parseYaml(Class clazz, String yamlDoc, String name) { 28 | 29 | LoaderOptions options = new LoaderOptions(); 30 | Yaml snake = new Yaml(new Constructor(clazz)); 31 | T entity = null; 32 | try { 33 | entity = snake.load(yamlDoc); 34 | } catch (YAMLException ex) { 35 | String msg = "Unable to parse yaml definition of configmap, check if you don't have typo: \n'\n" + 36 | yamlDoc + "\n'\n"; 37 | log.error(msg); 38 | throw new IllegalStateException(ex); 39 | } 40 | if (entity == null) { 41 | String msg = "Unable to parse yaml definition of configmap, check if you don't have typo: \n'\n" + 42 | yamlDoc + "\n'\n"; 43 | log.error(msg); 44 | try { 45 | entity = clazz.newInstance(); 46 | } catch (InstantiationException e) { 47 | e.printStackTrace(); 48 | } catch (IllegalAccessException e) { 49 | e.printStackTrace(); 50 | } 51 | } 52 | if (entity != null && entity.getName() == null) { 53 | entity.setName(name); 54 | } 55 | return entity; 56 | } 57 | 58 | /** 59 | * 60 | * @param clazz concrete class of type T that extends the EntityInfo. 61 | * This is the resulting type, we are convertion into. 62 | * @param cm input config map that will be converted into T. 63 | * We assume there is a multi-line section in the config map called config and it 64 | * contains a YAML structure that represents the object of type T. In other words the 65 | * keys in the yaml should be the same as the field names in the class T and the name of the 66 | * configmap will be assigned to the name of the object T. One can create arbitrarily deep 67 | * configuration by nesting the types in T and using the Snake yaml as the conversion library. 68 | * @param type parameter (T must extend {@link io.radanalytics.operator.common.EntityInfo}) 69 | * @return Java object of type T 70 | */ 71 | public static T parseCM(Class clazz, ConfigMap cm) { 72 | String yaml = cm.getData().get("config"); 73 | T entity = parseYaml(clazz, yaml, cm.getMetadata().getName()); 74 | return entity; 75 | } 76 | } 77 | -------------------------------------------------------------------------------- /src/main/java/io/radanalytics/operator/common/ConfigMapWatcher.java: -------------------------------------------------------------------------------- 1 | package io.radanalytics.operator.common; 2 | 3 | import io.fabric8.kubernetes.api.model.ConfigMap; 4 | import io.fabric8.kubernetes.api.model.apiextensions.CustomResourceDefinition; 5 | import io.fabric8.kubernetes.client.KubernetesClient; 6 | import io.radanalytics.operator.resource.HasDataHelper; 7 | 8 | import java.util.Map; 9 | import java.util.concurrent.CompletableFuture; 10 | import java.util.function.BiConsumer; 11 | import java.util.function.Function; 12 | import java.util.function.Predicate; 13 | 14 | import static io.radanalytics.operator.common.OperatorConfig.ALL_NAMESPACES; 15 | 16 | public class ConfigMapWatcher extends AbstractWatcher { 17 | 18 | // use via builder 19 | private ConfigMapWatcher(String namespace, 20 | String entityName, 21 | KubernetesClient client, 22 | Map selector, 23 | BiConsumer onAdd, 24 | BiConsumer onDelete, 25 | BiConsumer onModify, 26 | Predicate predicate, 27 | Function convert) { 28 | super(true, namespace, entityName, client, null, selector, onAdd, onDelete, onModify, predicate, convert, null); 29 | } 30 | 31 | public static class Builder { 32 | private boolean registered = false; 33 | private String namespace = ALL_NAMESPACES; 34 | private String entityName; 35 | private KubernetesClient client; 36 | private Map selector; 37 | 38 | private BiConsumer onAdd; 39 | private BiConsumer onDelete; 40 | private BiConsumer onModify; 41 | private Predicate predicate; 42 | private Function convert; 43 | 44 | public Builder withNamespace(String namespace) { 45 | this.namespace = namespace; 46 | return this; 47 | } 48 | 49 | public Builder withEntityName(String entityName) { 50 | this.entityName = entityName; 51 | return this; 52 | } 53 | 54 | public Builder withClient(KubernetesClient client) { 55 | this.client = client; 56 | return this; 57 | } 58 | 59 | public Builder withSelector(Map selector) { 60 | this.selector = selector; 61 | return this; 62 | } 63 | 64 | public Builder withOnAdd(BiConsumer onAdd) { 65 | this.onAdd = onAdd; 66 | return this; 67 | } 68 | 69 | public Builder withOnDelete(BiConsumer onDelete) { 70 | this.onDelete = onDelete; 71 | return this; 72 | } 73 | 74 | public Builder withOnModify(BiConsumer onModify) { 75 | this.onModify = onModify; 76 | return this; 77 | } 78 | 79 | public Builder withPredicate(Predicate predicate) { 80 | this.predicate = predicate; 81 | return this; 82 | } 83 | 84 | public Builder withConvert(Function convert) { 85 | this.convert = convert; 86 | return this; 87 | } 88 | 89 | public ConfigMapWatcher build() { 90 | if (!registered) { 91 | io.fabric8.kubernetes.internal.KubernetesDeserializer.registerCustomKind("v1#ConfigMap", ConfigMap.class); 92 | registered = true; 93 | } 94 | return new ConfigMapWatcher(namespace, entityName, client, selector, onAdd, onDelete, onModify, predicate, convert); 95 | } 96 | } 97 | 98 | public static T defaultConvert(Class clazz, ConfigMap cm) { 99 | return HasDataHelper.parseCM(clazz, cm); 100 | } 101 | 102 | @Override 103 | public CompletableFuture> watch() { 104 | return createConfigMapWatch().thenApply(watch -> this); 105 | } 106 | } 107 | 108 | 109 | -------------------------------------------------------------------------------- /src/main/java/io/radanalytics/operator/common/CustomResourceWatcher.java: -------------------------------------------------------------------------------- 1 | package io.radanalytics.operator.common; 2 | 3 | import com.fasterxml.jackson.databind.ObjectMapper; 4 | import io.fabric8.kubernetes.api.model.apiextensions.CustomResourceDefinition; 5 | import io.fabric8.kubernetes.client.KubernetesClient; 6 | import io.radanalytics.operator.common.crd.InfoClass; 7 | 8 | import java.util.concurrent.CompletableFuture; 9 | import java.util.function.BiConsumer; 10 | import java.util.function.Function; 11 | 12 | import static io.radanalytics.operator.common.OperatorConfig.ALL_NAMESPACES; 13 | 14 | public class CustomResourceWatcher extends AbstractWatcher { 15 | 16 | // use via builder 17 | private CustomResourceWatcher(String namespace, 18 | String entityName, 19 | KubernetesClient client, 20 | CustomResourceDefinition crd, 21 | BiConsumer onAdd, 22 | BiConsumer onDelete, 23 | BiConsumer onModify, 24 | Function convert) { 25 | super(true, namespace, entityName, client, crd, null, onAdd, onDelete, onModify, null, null, convert); 26 | } 27 | 28 | public static class Builder { 29 | private String namespace = ALL_NAMESPACES; 30 | private String entityName; 31 | private KubernetesClient client; 32 | private CustomResourceDefinition crd; 33 | 34 | private BiConsumer onAdd; 35 | private BiConsumer onDelete; 36 | private BiConsumer onModify; 37 | private Function convert; 38 | 39 | public Builder withNamespace(String namespace) { 40 | this.namespace = namespace; 41 | return this; 42 | } 43 | 44 | public Builder withEntityName(String entityName) { 45 | this.entityName = entityName; 46 | return this; 47 | } 48 | 49 | public Builder withClient(KubernetesClient client) { 50 | this.client = client; 51 | return this; 52 | } 53 | 54 | public Builder withCrd(CustomResourceDefinition crd) { 55 | this.crd = crd; 56 | return this; 57 | } 58 | 59 | public Builder withOnAdd(BiConsumer onAdd) { 60 | this.onAdd = onAdd; 61 | return this; 62 | } 63 | 64 | public Builder withOnDelete(BiConsumer onDelete) { 65 | this.onDelete = onDelete; 66 | return this; 67 | } 68 | 69 | public Builder withOnModify(BiConsumer onModify) { 70 | this.onModify = onModify; 71 | return this; 72 | } 73 | 74 | public Builder withConvert(Function convert) { 75 | this.convert = convert; 76 | return this; 77 | } 78 | 79 | public CustomResourceWatcher build() { 80 | return new CustomResourceWatcher(namespace, entityName, client, crd, onAdd, onDelete, onModify, convert); 81 | } 82 | } 83 | 84 | public static T defaultConvert(Class clazz, InfoClass info) { 85 | String name = info.getMetadata().getName(); 86 | String namespace = info.getMetadata().getNamespace(); 87 | ObjectMapper mapper = new ObjectMapper(); 88 | T infoSpec = mapper.convertValue(info.getSpec(), clazz); 89 | if (infoSpec == null) { // empty spec 90 | try { 91 | infoSpec = clazz.newInstance(); 92 | } catch (InstantiationException e) { 93 | e.printStackTrace(); 94 | } catch (IllegalAccessException e) { 95 | e.printStackTrace(); 96 | } 97 | } 98 | if (infoSpec.getName() == null) { 99 | infoSpec.setName(name); 100 | } 101 | if (infoSpec.getNamespace() == null) { 102 | infoSpec.setNamespace(namespace); 103 | } 104 | return infoSpec; 105 | } 106 | 107 | @Override 108 | public CompletableFuture> watch() { 109 | return createCustomResourceWatch().thenApply(watch -> this); 110 | } 111 | } 112 | 113 | 114 | -------------------------------------------------------------------------------- /.mvn/wrapper/MavenWrapperDownloader.java: -------------------------------------------------------------------------------- 1 | /* 2 | Licensed to the Apache Software Foundation (ASF) under one 3 | or more contributor license agreements. See the NOTICE file 4 | distributed with this work for additional information 5 | regarding copyright ownership. The ASF licenses this file 6 | to you under the Apache License, Version 2.0 (the 7 | "License"); you may not use this file except in compliance 8 | with the License. You may obtain a copy of the License at 9 | 10 | http://www.apache.org/licenses/LICENSE-2.0 11 | 12 | Unless required by applicable law or agreed to in writing, 13 | software distributed under the License is distributed on an 14 | "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY 15 | KIND, either express or implied. See the License for the 16 | specific language governing permissions and limitations 17 | under the License. 18 | */ 19 | 20 | import java.net.*; 21 | import java.io.*; 22 | import java.nio.channels.*; 23 | import java.util.Properties; 24 | 25 | public class MavenWrapperDownloader { 26 | 27 | /** 28 | * Default URL to download the maven-wrapper.jar from, if no 'downloadUrl' is provided. 29 | */ 30 | private static final String DEFAULT_DOWNLOAD_URL = 31 | "https://repo.maven.apache.org/maven2/io/takari/maven-wrapper/0.4.0/maven-wrapper-0.4.0.jar"; 32 | 33 | /** 34 | * Path to the maven-wrapper.properties file, which might contain a downloadUrl property to 35 | * use instead of the default one. 36 | */ 37 | private static final String MAVEN_WRAPPER_PROPERTIES_PATH = 38 | ".mvn/wrapper/maven-wrapper.properties"; 39 | 40 | /** 41 | * Path where the maven-wrapper.jar will be saved to. 42 | */ 43 | private static final String MAVEN_WRAPPER_JAR_PATH = 44 | ".mvn/wrapper/maven-wrapper.jar"; 45 | 46 | /** 47 | * Name of the property which should be used to override the default download url for the wrapper. 48 | */ 49 | private static final String PROPERTY_NAME_WRAPPER_URL = "wrapperUrl"; 50 | 51 | public static void main(String args[]) { 52 | System.out.println("- Downloader started"); 53 | File baseDirectory = new File(args[0]); 54 | System.out.println("- Using base directory: " + baseDirectory.getAbsolutePath()); 55 | 56 | // If the maven-wrapper.properties exists, read it and check if it contains a custom 57 | // wrapperUrl parameter. 58 | File mavenWrapperPropertyFile = new File(baseDirectory, MAVEN_WRAPPER_PROPERTIES_PATH); 59 | String url = DEFAULT_DOWNLOAD_URL; 60 | if(mavenWrapperPropertyFile.exists()) { 61 | FileInputStream mavenWrapperPropertyFileInputStream = null; 62 | try { 63 | mavenWrapperPropertyFileInputStream = new FileInputStream(mavenWrapperPropertyFile); 64 | Properties mavenWrapperProperties = new Properties(); 65 | mavenWrapperProperties.load(mavenWrapperPropertyFileInputStream); 66 | url = mavenWrapperProperties.getProperty(PROPERTY_NAME_WRAPPER_URL, url); 67 | } catch (IOException e) { 68 | System.out.println("- ERROR loading '" + MAVEN_WRAPPER_PROPERTIES_PATH + "'"); 69 | } finally { 70 | try { 71 | if(mavenWrapperPropertyFileInputStream != null) { 72 | mavenWrapperPropertyFileInputStream.close(); 73 | } 74 | } catch (IOException e) { 75 | // Ignore ... 76 | } 77 | } 78 | } 79 | System.out.println("- Downloading from: : " + url); 80 | 81 | File outputFile = new File(baseDirectory.getAbsolutePath(), MAVEN_WRAPPER_JAR_PATH); 82 | if(!outputFile.getParentFile().exists()) { 83 | if(!outputFile.getParentFile().mkdirs()) { 84 | System.out.println( 85 | "- ERROR creating output direcrory '" + outputFile.getParentFile().getAbsolutePath() + "'"); 86 | } 87 | } 88 | System.out.println("- Downloading to: " + outputFile.getAbsolutePath()); 89 | try { 90 | downloadFileFromURL(url, outputFile); 91 | System.out.println("Done"); 92 | System.exit(0); 93 | } catch (Throwable e) { 94 | System.out.println("- Error downloading"); 95 | e.printStackTrace(); 96 | System.exit(1); 97 | } 98 | } 99 | 100 | private static void downloadFileFromURL(String urlString, File destination) throws Exception { 101 | URL website = new URL(urlString); 102 | ReadableByteChannel rbc; 103 | rbc = Channels.newChannel(website.openStream()); 104 | FileOutputStream fos = new FileOutputStream(destination); 105 | fos.getChannel().transferFrom(rbc, 0, Long.MAX_VALUE); 106 | fos.close(); 107 | rbc.close(); 108 | } 109 | 110 | } 111 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # abstract-operator 2 | 3 | [![Build status](https://travis-ci.org/jvm-operators/abstract-operator.svg?branch=master)](https://travis-ci.org/jvm-operators/abstract-operator) 4 | [![License](https://img.shields.io/badge/license-Apache--2.0-blue.svg)](http://www.apache.org/licenses/LICENSE-2.0) 5 | 6 | `{CRD|ConfigMap}`-based approach for lyfecycle management of various resources in Kubernetes and OpenShift. Using the Operator pattern, you can leverage the Kubernetes control loop and react on various events in the cluster. The idea of the operator patern is to encapsulate the operational knowledge into the abovementioned control loop and declarative approach. 7 | 8 | ## Example Implementations 9 | * [spark-operator](https://github.com/radanalyticsio/spark-operator) - Java operator for managing Apache Spark clusters and apps 10 | * [fdp-flink-operator](https://github.com/lightbend/fdp-flink-operator) - Scala operator for managing Apache Flink clusters ([job](https://ci.apache.org/projects/flink/flink-docs-stable/ops/deployment/kubernetes.html#flink-job-cluster-on-kubernetes) and [session](https://ci.apache.org/projects/flink/flink-docs-stable/ops/deployment/kubernetes.html#flink-session-cluster-on-kubernetes)) 11 | * [java-example-operator](https://github.com/jvm-operators/java-example-operator) - Minimalistic operator in Java 12 | * [scala-example-operator](https://github.com/jvm-operators/scala-example-operator) - Minimalistic operator in Scala 13 | * [kotlin-example-operator](https://github.com/jvm-operators/kotlin-example-operator) - Minimalistic operator in Kotlin 14 | * [groovy-example-operator](https://github.com/jvm-operators/groovy-example-operator) - Minimalistic operator in Groovy 15 | * [haskell-example-operator](https://github.com/jvm-operators/haskell-example-operator) - Minimalistic operator in Haskell (Frege) and Groovy 16 | * [javascript-example-operator](https://github.com/jvm-operators/javascript-example-operator) - Minimalistic operator in EcmaScript 17 | 18 | ## Code 19 | This library can be simply used by adding it to classpath; creating a new class that extends `AbstractOperator`. This 'concrete operator' class needs to also have the `@Operator` annotation on it. For capturing the information about the monitored resources one has to also create a class that extends `EntityInfo` and have arbitrary fields on it with getters and setters. 20 | 21 | This class can be also generated from the JSON schema. To do that add [jsonschema2pojo](https://github.com/radanalyticsio/spark-operator/blob/4f72e740ea2126843b0c240bd800a74169d5f1c2/pom.xml#L50:L53) plugin to the pom.xml and json schema to resources ([example](https://github.com/radanalyticsio/spark-operator/tree/4f72e740ea2126843b0c240bd800a74169d5f1c2/src/main/resources/schema)). 22 | 23 | This is a no-op operator in Scala that simply logs into console when config map with label `radanalytics.io/kind = foo` is created. 24 | 25 | ```Scala 26 | @Operator(forKind = "foo", prefix = "radanalytics.io", infoClass = classOf[FooInfo]) 27 | class FooOperator extends AbstractOperator[FooInfo] { 28 | val log: Logger = LoggerFactory.getLogger(classOf[FooInfo].getName) 29 | 30 | @Override 31 | def onAdd(foo: FooInfo) = { 32 | log.info(s"created foo with name ${foo.name} and someParameter = ${foo.someParameter}") 33 | } 34 | 35 | @Override 36 | def onDelete(foo: FooInfo) = { 37 | log.info(s"deleted foo with name ${foo.name} and someParameter = ${foo.someParameter}") 38 | } 39 | } 40 | ``` 41 | 42 | ### CRDs 43 | By default the operator is based on `CustomResources`, if you want to create `ConfigMap`-based operator, add `crd=false` parameter in the `@Operator` annotation. The CRD mode will try to create the custom resource definition from the `infoClass` if it's not already there and then it listens on the newly created, deleted or modified custom resources (CR) of the given type. 44 | 45 | For the CRDs the: 46 | * `forKind` field represent the name of the `CRD` ('s' at the end for plural) 47 | * `prefix` field in the annotation represents the group 48 | * `shortNames` field is an array of strings representing the shortened name of the resource. 49 | * `pluralName` field is a string value representing the plural name for the resource. 50 | * `enabled` field is a boolean value (default is `true`), if disabled the operator is silenced 51 | * as for the version, currently the `v1` is created automatically, but one can also create the `CRD` on his own before running the operator and providing the `forKind` and `prefix` matches, operator will use the existing `CRD` 52 | 53 | #### Configuration 54 | You can configure the operator using some environmental variables. Here is the list: 55 | * `WATCH_NAMESPACE`, example values `myproject`, `foo,bar,baz`, `*` - what namespaces the operator should be watching for the events, 56 | if left empty, all namespace will be used (same as `*`). One may also use special value `~` denoting the same namespace where the operator is deployed in. 57 | * `CRD`, values `true/false` - config maps = `false`, custom resources = `true`, default: `true` 58 | * `COLORS`, values `true/false` - if `true` there will be colors used in the log file, default: `true` 59 | * `METRICS`, values `true/false` - whether start the simple http server that exposes internal metrics. These metrics are in the Prometheus compliant format and can be scraped by Prometheus; default: `true` 60 | * `METRICS_JVM`, values `true/false` - whether expose also internal JVM metrics such as heap usage, number of threads and similar; default: `false` 61 | * `METRICS_PORT`, example values `1337`; default: `8080` 62 | 63 | 64 | ## Documentation 65 | [javadoc](https://jvm-operators.github.io/abstract-operator/) 66 | -------------------------------------------------------------------------------- /mvnw.cmd: -------------------------------------------------------------------------------- 1 | @REM ---------------------------------------------------------------------------- 2 | @REM Licensed to the Apache Software Foundation (ASF) under one 3 | @REM or more contributor license agreements. See the NOTICE file 4 | @REM distributed with this work for additional information 5 | @REM regarding copyright ownership. The ASF licenses this file 6 | @REM to you under the Apache License, Version 2.0 (the 7 | @REM "License"); you may not use this file except in compliance 8 | @REM with the License. You may obtain a copy of the License at 9 | @REM 10 | @REM http://www.apache.org/licenses/LICENSE-2.0 11 | @REM 12 | @REM Unless required by applicable law or agreed to in writing, 13 | @REM software distributed under the License is distributed on an 14 | @REM "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY 15 | @REM KIND, either express or implied. See the License for the 16 | @REM specific language governing permissions and limitations 17 | @REM under the License. 18 | @REM ---------------------------------------------------------------------------- 19 | 20 | @REM ---------------------------------------------------------------------------- 21 | @REM Maven2 Start Up Batch script 22 | @REM 23 | @REM Required ENV vars: 24 | @REM JAVA_HOME - location of a JDK home dir 25 | @REM 26 | @REM Optional ENV vars 27 | @REM M2_HOME - location of maven2's installed home dir 28 | @REM MAVEN_BATCH_ECHO - set to 'on' to enable the echoing of the batch commands 29 | @REM MAVEN_BATCH_PAUSE - set to 'on' to wait for a key stroke before ending 30 | @REM MAVEN_OPTS - parameters passed to the Java VM when running Maven 31 | @REM e.g. to debug Maven itself, use 32 | @REM set MAVEN_OPTS=-Xdebug -Xrunjdwp:transport=dt_socket,server=y,suspend=y,address=8000 33 | @REM MAVEN_SKIP_RC - flag to disable loading of mavenrc files 34 | @REM ---------------------------------------------------------------------------- 35 | 36 | @REM Begin all REM lines with '@' in case MAVEN_BATCH_ECHO is 'on' 37 | @echo off 38 | @REM set title of command window 39 | title %0 40 | @REM enable echoing my setting MAVEN_BATCH_ECHO to 'on' 41 | @if "%MAVEN_BATCH_ECHO%" == "on" echo %MAVEN_BATCH_ECHO% 42 | 43 | @REM set %HOME% to equivalent of $HOME 44 | if "%HOME%" == "" (set "HOME=%HOMEDRIVE%%HOMEPATH%") 45 | 46 | @REM Execute a user defined script before this one 47 | if not "%MAVEN_SKIP_RC%" == "" goto skipRcPre 48 | @REM check for pre script, once with legacy .bat ending and once with .cmd ending 49 | if exist "%HOME%\mavenrc_pre.bat" call "%HOME%\mavenrc_pre.bat" 50 | if exist "%HOME%\mavenrc_pre.cmd" call "%HOME%\mavenrc_pre.cmd" 51 | :skipRcPre 52 | 53 | @setlocal 54 | 55 | set ERROR_CODE=0 56 | 57 | @REM To isolate internal variables from possible post scripts, we use another setlocal 58 | @setlocal 59 | 60 | @REM ==== START VALIDATION ==== 61 | if not "%JAVA_HOME%" == "" goto OkJHome 62 | 63 | echo. 64 | echo Error: JAVA_HOME not found in your environment. >&2 65 | echo Please set the JAVA_HOME variable in your environment to match the >&2 66 | echo location of your Java installation. >&2 67 | echo. 68 | goto error 69 | 70 | :OkJHome 71 | if exist "%JAVA_HOME%\bin\java.exe" goto init 72 | 73 | echo. 74 | echo Error: JAVA_HOME is set to an invalid directory. >&2 75 | echo JAVA_HOME = "%JAVA_HOME%" >&2 76 | echo Please set the JAVA_HOME variable in your environment to match the >&2 77 | echo location of your Java installation. >&2 78 | echo. 79 | goto error 80 | 81 | @REM ==== END VALIDATION ==== 82 | 83 | :init 84 | 85 | @REM Find the project base dir, i.e. the directory that contains the folder ".mvn". 86 | @REM Fallback to current working directory if not found. 87 | 88 | set MAVEN_PROJECTBASEDIR=%MAVEN_BASEDIR% 89 | IF NOT "%MAVEN_PROJECTBASEDIR%"=="" goto endDetectBaseDir 90 | 91 | set EXEC_DIR=%CD% 92 | set WDIR=%EXEC_DIR% 93 | :findBaseDir 94 | IF EXIST "%WDIR%"\.mvn goto baseDirFound 95 | cd .. 96 | IF "%WDIR%"=="%CD%" goto baseDirNotFound 97 | set WDIR=%CD% 98 | goto findBaseDir 99 | 100 | :baseDirFound 101 | set MAVEN_PROJECTBASEDIR=%WDIR% 102 | cd "%EXEC_DIR%" 103 | goto endDetectBaseDir 104 | 105 | :baseDirNotFound 106 | set MAVEN_PROJECTBASEDIR=%EXEC_DIR% 107 | cd "%EXEC_DIR%" 108 | 109 | :endDetectBaseDir 110 | 111 | IF NOT EXIST "%MAVEN_PROJECTBASEDIR%\.mvn\jvm.config" goto endReadAdditionalConfig 112 | 113 | @setlocal EnableExtensions EnableDelayedExpansion 114 | for /F "usebackq delims=" %%a in ("%MAVEN_PROJECTBASEDIR%\.mvn\jvm.config") do set JVM_CONFIG_MAVEN_PROPS=!JVM_CONFIG_MAVEN_PROPS! %%a 115 | @endlocal & set JVM_CONFIG_MAVEN_PROPS=%JVM_CONFIG_MAVEN_PROPS% 116 | 117 | :endReadAdditionalConfig 118 | 119 | SET MAVEN_JAVA_EXE="%JAVA_HOME%\bin\java.exe" 120 | set WRAPPER_JAR="%MAVEN_PROJECTBASEDIR%\.mvn\wrapper\maven-wrapper.jar" 121 | set WRAPPER_LAUNCHER=org.apache.maven.wrapper.MavenWrapperMain 122 | 123 | set DOWNLOAD_URL="https://repo.maven.apache.org/maven2/io/takari/maven-wrapper/0.4.0/maven-wrapper-0.4.0.jar" 124 | FOR /F "tokens=1,2 delims==" %%A IN (%MAVEN_PROJECTBASEDIR%\.mvn\wrapper\maven-wrapper.properties) DO ( 125 | IF "%%A"=="wrapperUrl" SET DOWNLOAD_URL=%%B 126 | ) 127 | 128 | @REM Extension to allow automatically downloading the maven-wrapper.jar from Maven-central 129 | @REM This allows using the maven wrapper in projects that prohibit checking in binary data. 130 | if exist %WRAPPER_JAR% ( 131 | echo Found %WRAPPER_JAR% 132 | ) else ( 133 | echo Couldn't find %WRAPPER_JAR%, downloading it ... 134 | echo Downloading from: %DOWNLOAD_URL% 135 | powershell -Command "(New-Object Net.WebClient).DownloadFile('%DOWNLOAD_URL%', '%WRAPPER_JAR%')" 136 | echo Finished downloading %WRAPPER_JAR% 137 | ) 138 | @REM End of extension 139 | 140 | %MAVEN_JAVA_EXE% %JVM_CONFIG_MAVEN_PROPS% %MAVEN_OPTS% %MAVEN_DEBUG_OPTS% -classpath %WRAPPER_JAR% "-Dmaven.multiModuleProjectDirectory=%MAVEN_PROJECTBASEDIR%" %WRAPPER_LAUNCHER% %MAVEN_CONFIG% %* 141 | if ERRORLEVEL 1 goto error 142 | goto end 143 | 144 | :error 145 | set ERROR_CODE=1 146 | 147 | :end 148 | @endlocal & set ERROR_CODE=%ERROR_CODE% 149 | 150 | if not "%MAVEN_SKIP_RC%" == "" goto skipRcPost 151 | @REM check for post script, once with legacy .bat ending and once with .cmd ending 152 | if exist "%HOME%\mavenrc_post.bat" call "%HOME%\mavenrc_post.bat" 153 | if exist "%HOME%\mavenrc_post.cmd" call "%HOME%\mavenrc_post.cmd" 154 | :skipRcPost 155 | 156 | @REM pause the script if MAVEN_BATCH_PAUSE is set to 'on' 157 | if "%MAVEN_BATCH_PAUSE%" == "on" pause 158 | 159 | if "%MAVEN_TERMINATE_CMD%" == "on" exit %ERROR_CODE% 160 | 161 | exit /B %ERROR_CODE% 162 | -------------------------------------------------------------------------------- /src/main/java/io/radanalytics/operator/common/OperatorConfig.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2017-2018 3 | * License: Apache License 2.0 (see the file LICENSE or http://apache.org/licenses/LICENSE-2.0.html). 4 | */ 5 | package io.radanalytics.operator.common; 6 | 7 | import java.util.Collections; 8 | import java.util.HashSet; 9 | import java.util.Map; 10 | import java.util.Set; 11 | import java.util.stream.Collectors; 12 | 13 | import static java.util.Arrays.asList; 14 | 15 | /** 16 | * Operator configuration 17 | */ 18 | public class OperatorConfig { 19 | 20 | public static final String WATCH_NAMESPACE = "WATCH_NAMESPACE"; 21 | public static final String SAME_NAMESPACE = "~"; 22 | public static final String ALL_NAMESPACES = "*"; 23 | public static final String METRICS = "METRICS"; 24 | public static final String METRICS_JVM = "METRICS_JVM"; 25 | public static final String METRICS_PORT = "METRICS_PORT"; 26 | public static final String FULL_RECONCILIATION_INTERVAL_S = "FULL_RECONCILIATION_INTERVAL_S"; 27 | public static final String OPERATOR_OPERATION_TIMEOUT_MS = "OPERATOR_OPERATION_TIMEOUT_MS"; 28 | 29 | public static final boolean DEFAULT_METRICS = true; 30 | public static final boolean DEFAULT_METRICS_JVM = false; 31 | public static final int DEFAULT_METRICS_PORT = 8080; 32 | public static final long DEFAULT_FULL_RECONCILIATION_INTERVAL_S = 180; 33 | public static final long DEFAULT_OPERATION_TIMEOUT_MS = 60_000; 34 | 35 | private final Set namespaces; 36 | private final boolean metrics; 37 | private final boolean metricsJvm; 38 | private final int metricsPort; 39 | private final long reconciliationIntervalS; 40 | private final long operationTimeoutMs; 41 | 42 | /** 43 | * Constructor 44 | * 45 | * @param namespaces namespace in which the operator will run and create resources 46 | * @param metrics whether the metrics server for prometheus should be started 47 | * @param metricsJvm whether to expose the internal JVM metrics, like heap, # of threads, etc. 48 | * @param metricsPort on which port the metrics server should be listening 49 | * @param reconciliationIntervalS specify every how many milliseconds the reconciliation runs 50 | * @param operationTimeoutMs timeout for internal operations specified in milliseconds 51 | */ 52 | public OperatorConfig(Set namespaces, boolean metrics, boolean metricsJvm, int metricsPort, 53 | long reconciliationIntervalS, long operationTimeoutMs) { 54 | this.namespaces = namespaces; 55 | this.reconciliationIntervalS = reconciliationIntervalS; 56 | this.operationTimeoutMs = operationTimeoutMs; 57 | this.metrics = metrics; 58 | this.metricsJvm = metricsJvm; 59 | this.metricsPort = metricsPort; 60 | } 61 | 62 | /** 63 | * Loads configuration parameters from a related map 64 | * 65 | * @param map map from which loading configuration parameters 66 | * @return Cluster Operator configuration instance 67 | */ 68 | public static OperatorConfig fromMap(Map map) { 69 | 70 | String namespacesList = map.get(WATCH_NAMESPACE); 71 | 72 | Set namespaces; 73 | if (namespacesList == null || namespacesList.isEmpty()) { 74 | // empty WATCH_NAMESPACE means we will be watching all the namespaces 75 | namespaces = Collections.singleton(ALL_NAMESPACES); 76 | } else { 77 | namespaces = new HashSet<>(asList(namespacesList.trim().split("\\s*,+\\s*"))); 78 | namespaces = namespaces.stream().map( 79 | ns -> ns.startsWith("\"") && ns.endsWith("\"") ? ns.substring(1, ns.length() - 1) : ns) 80 | .collect(Collectors.toSet()); 81 | } 82 | 83 | boolean metricsAux = DEFAULT_METRICS; 84 | String metricsEnvVar = map.get(METRICS); 85 | if (metricsEnvVar != null) { 86 | metricsAux = !"false".equals(metricsEnvVar.trim().toLowerCase()); 87 | } 88 | 89 | boolean metricsJvmAux = DEFAULT_METRICS_JVM; 90 | int metricsPortAux = DEFAULT_METRICS_PORT; 91 | if (metricsAux) { 92 | String metricsJvmEnvVar = map.get(METRICS_JVM); 93 | if (metricsJvmEnvVar != null) { 94 | metricsJvmAux = "true".equals(metricsJvmEnvVar.trim().toLowerCase()); 95 | } 96 | String metricsPortEnvVar = map.get(METRICS_PORT); 97 | if (metricsPortEnvVar != null) { 98 | metricsPortAux = Integer.parseInt(metricsPortEnvVar.trim().toLowerCase()); 99 | } 100 | } 101 | 102 | long reconciliationInterval = DEFAULT_FULL_RECONCILIATION_INTERVAL_S; 103 | String reconciliationIntervalEnvVar = map.get(FULL_RECONCILIATION_INTERVAL_S); 104 | if (reconciliationIntervalEnvVar != null) { 105 | reconciliationInterval = Long.parseLong(reconciliationIntervalEnvVar); 106 | } 107 | 108 | long operationTimeout = DEFAULT_OPERATION_TIMEOUT_MS; 109 | String operationTimeoutEnvVar = map.get(OPERATOR_OPERATION_TIMEOUT_MS); 110 | if (operationTimeoutEnvVar != null) { 111 | operationTimeout = Long.parseLong(operationTimeoutEnvVar); 112 | } 113 | 114 | return new OperatorConfig(namespaces, metricsAux, metricsJvmAux, metricsPortAux, reconciliationInterval, 115 | operationTimeout); 116 | } 117 | 118 | 119 | /** 120 | * @return namespaces in which the operator runs and creates resources 121 | */ 122 | public Set getNamespaces() { 123 | return namespaces; 124 | } 125 | 126 | /** 127 | * @return how many seconds among the reconciliation runs 128 | */ 129 | public long getReconciliationIntervalS() { 130 | return reconciliationIntervalS; 131 | } 132 | 133 | /** 134 | * @return how many milliseconds should we wait for Kubernetes operations 135 | */ 136 | public long getOperationTimeoutMs() { 137 | return operationTimeoutMs; 138 | } 139 | 140 | public boolean isMetrics() { 141 | return metrics; 142 | } 143 | 144 | public boolean isMetricsJvm() { 145 | return metricsJvm; 146 | } 147 | 148 | public int getMetricsPort() { 149 | return metricsPort; 150 | } 151 | 152 | @Override 153 | public String toString() { 154 | return "OperatorConfig{" + 155 | "namespaces=" + namespaces + 156 | ", metrics=" + metrics + 157 | ", metricsJvm=" + metricsJvm + 158 | ", metricsPort=" + metricsPort + 159 | ", reconciliationIntervalS=" + reconciliationIntervalS + 160 | ", operationTimeoutMs=" + operationTimeoutMs + 161 | '}'; 162 | } 163 | } 164 | -------------------------------------------------------------------------------- /src/main/java/io/radanalytics/operator/common/crd/CrdDeployer.java: -------------------------------------------------------------------------------- 1 | package io.radanalytics.operator.common.crd; 2 | 3 | import com.fasterxml.jackson.databind.DeserializationFeature; 4 | import io.fabric8.kubernetes.api.model.apiextensions.CustomResourceDefinition; 5 | import io.fabric8.kubernetes.api.model.apiextensions.CustomResourceDefinitionBuilder; 6 | import io.fabric8.kubernetes.api.model.apiextensions.CustomResourceDefinitionFluent; 7 | import io.fabric8.kubernetes.api.model.apiextensions.CustomResourceSubresourceStatus; 8 | import io.fabric8.kubernetes.api.model.apiextensions.JSONSchemaProps; 9 | import io.fabric8.kubernetes.client.CustomResourceList; 10 | import io.fabric8.kubernetes.client.KubernetesClient; 11 | import io.fabric8.kubernetes.client.KubernetesClientException; 12 | import io.fabric8.kubernetes.client.utils.Serialization; 13 | import io.radanalytics.operator.common.EntityInfo; 14 | import io.radanalytics.operator.common.JSONSchemaReader; 15 | import org.slf4j.Logger; 16 | 17 | import javax.inject.Inject; 18 | import javax.inject.Singleton; 19 | import java.util.Arrays; 20 | import java.util.List; 21 | import java.util.stream.Collectors; 22 | 23 | @Singleton 24 | public class CrdDeployer { 25 | 26 | @Inject 27 | protected Logger log; 28 | 29 | public CustomResourceDefinition initCrds(KubernetesClient client, 30 | String prefix, 31 | String entityName, 32 | String[] shortNames, 33 | String pluralName, 34 | String[] additionalPrinterColumnNames, 35 | String[] additionalPrinterColumnPaths, 36 | String[] additionalPrinterColumnTypes, 37 | Class infoClass, 38 | boolean isOpenshift) { 39 | final String newPrefix = prefix.substring(0, prefix.length() - 1); 40 | CustomResourceDefinition crdToReturn; 41 | 42 | Serialization.jsonMapper().configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false); 43 | List crds = client.customResourceDefinitions() 44 | .list() 45 | .getItems() 46 | .stream() 47 | .filter(p -> entityName.equals(p.getSpec().getNames().getKind()) && newPrefix.equals(p.getSpec().getGroup())) 48 | .collect(Collectors.toList()); 49 | if (!crds.isEmpty()) { 50 | crdToReturn = crds.get(0); 51 | log.info("CustomResourceDefinition for {} has been found in the K8s, so we are skipping the creation.", entityName); 52 | } else { 53 | log.info("Creating CustomResourceDefinition for {}.", entityName); 54 | JSONSchemaProps schema = JSONSchemaReader.readSchema(infoClass); 55 | CustomResourceDefinitionFluent.SpecNested builder; 56 | 57 | if (schema != null) { 58 | removeDefaultValues(schema); 59 | builder = getCRDBuilder(newPrefix, 60 | entityName, 61 | shortNames, 62 | pluralName) 63 | .withNewValidation() 64 | .withNewOpenAPIV3SchemaLike(schema) 65 | .endOpenAPIV3Schema() 66 | .endValidation(); 67 | } else { 68 | builder = getCRDBuilder(newPrefix, 69 | entityName, 70 | shortNames, 71 | pluralName); 72 | } 73 | if (additionalPrinterColumnNames != null && additionalPrinterColumnNames.length > 0) { 74 | for (int i = 0; i < additionalPrinterColumnNames.length; i++) { 75 | builder = builder.addNewAdditionalPrinterColumn().withName(additionalPrinterColumnNames[i]).withJSONPath(additionalPrinterColumnPaths[i]).endAdditionalPrinterColumn(); 76 | } 77 | } 78 | crdToReturn = builder.endSpec().build(); 79 | try { 80 | if (schema != null) { 81 | // https://github.com/fabric8io/kubernetes-client/issues/1486 82 | crdToReturn.getSpec().getValidation().getOpenAPIV3Schema().setDependencies(null); 83 | } 84 | 85 | client.customResourceDefinitions().createOrReplace(crdToReturn); 86 | } catch (KubernetesClientException e) { 87 | // old version of K8s/openshift -> don't use schema validation 88 | log.warn("Consider upgrading the {}. Your version doesn't support schema validation for custom resources." 89 | , isOpenshift ? "OpenShift" : "Kubernetes"); 90 | crdToReturn = getCRDBuilder(newPrefix, 91 | entityName, 92 | shortNames, 93 | pluralName) 94 | .endSpec() 95 | .build(); 96 | client.customResourceDefinitions().createOrReplace(crdToReturn); 97 | } 98 | } 99 | 100 | // register the new crd for json serialization 101 | io.fabric8.kubernetes.internal.KubernetesDeserializer.registerCustomKind(newPrefix + "/" + crdToReturn.getSpec().getVersion() + "#" + entityName, InfoClass.class); 102 | io.fabric8.kubernetes.internal.KubernetesDeserializer.registerCustomKind(newPrefix + "/" + crdToReturn.getSpec().getVersion() + "#" + entityName + "List", CustomResourceList.class); 103 | 104 | return crdToReturn; 105 | } 106 | 107 | private void removeDefaultValues(JSONSchemaProps schema) { 108 | if (null == schema) { 109 | return; 110 | } 111 | schema.setDefault(null); 112 | if (null != schema.getProperties()) { 113 | for (JSONSchemaProps prop : schema.getProperties().values()) { 114 | removeDefaultValues(prop); 115 | } 116 | } 117 | } 118 | 119 | private CustomResourceDefinitionFluent.SpecNested getCRDBuilder(String prefix, 120 | String entityName, 121 | String[] shortNames, 122 | String pluralName) { 123 | // if no plural name is specified, try to make one by adding "s" 124 | // also, plural names must be all lowercase 125 | String plural = pluralName; 126 | if (plural.isEmpty()) { 127 | plural = (entityName + "s"); 128 | } 129 | plural = plural.toLowerCase(); 130 | 131 | // short names must be all lowercase 132 | String[] shortNamesLower = Arrays.stream(shortNames) 133 | .map(sn -> sn.toLowerCase()) 134 | .toArray(String[]::new); 135 | 136 | return new CustomResourceDefinitionBuilder() 137 | .withApiVersion("apiextensions.k8s.io/v1beta1") 138 | .withNewMetadata().withName(plural + "." + prefix) 139 | .endMetadata() 140 | .withNewSpec() 141 | .withNewNames() 142 | .withKind(entityName) 143 | .withPlural(plural) 144 | .withShortNames(Arrays.asList(shortNamesLower)).endNames() 145 | .withGroup(prefix) 146 | .withVersion("v1") 147 | .withScope("Namespaced") 148 | // add an empty status block to all CRDs created 149 | .withNewSubresources().withStatus(new CustomResourceSubresourceStatus()).endSubresources(); 150 | } 151 | } 152 | -------------------------------------------------------------------------------- /src/main/javadoc/overview.html: -------------------------------------------------------------------------------- 1 | 2 |
    3 |

    abstract-operator 9 |

    10 |

    Build status 15 | License

    19 |

    {ConfigMap|CRD}-based approach for lyfecycle management of various resources in Kubernetes and 20 | OpenShift. Using the Operator pattern, you can leverage the Kubernetes control loop and react on various events 21 | in the cluster.

    22 |

    Example Implementations 28 |

    29 | 40 |

    Code 46 |

    47 |

    This library can be simply used by adding it to classpath; creating a new class that extends AbstractOperator. 48 | This 'concrete operator' class needs to also have the @Operator annotation on it. For capturing the 49 | information about the monitored resources one has to also create a class that extends EntityInfo 50 | and have arbitrary fields on it with getters and setters.

    51 |

    This class can be also generated from the JSON schema. To do that add jsonschema2pojo 53 | plugin to the pom.xml and json schema to resources (example). 55 |

    56 |

    This is a no-op operator in Scala that simply logs into console when config map with label radanalytics.io/kind 57 | = foo is created.

    58 |
    @Operator(forKind = "foo", prefix = "radanalytics.io", infoClass = classOf[FooInfo])
     63 | class FooOperator extends AbstractOperator[FooInfo] {
     65 |   val log: Logger = LoggerFactory.getLogger(classOf[FooInfo].getName)
     68 | 
     69 |   @Override
     70 |   def onAdd(foo: FooInfo) = {
     72 |     log.info(s"created foo with name ${foo.name} and someParameter = ${foo.someParameter}")
     74 |   }
     75 | 
     76 |   @Override
     77 |   def onDelete(foo: FooInfo) = {
     79 |     log.info(s"deleted foo with name ${foo.name} and someParameter = ${foo.someParameter}")
     81 |   }
     82 | }
    83 |
    84 |

    CRDs 90 |

    91 |

    By default the operator is based on CustomResourceDefinitions, if you want to create ConfigMap-based 92 | operator, add crd=false parameter in the @Operator annotation. Custom Resource operation mode will try to create 93 | the custom resource definition from the infoClass if it's not already there and then it listens on 94 | the newly created, deleted or modified custom resources (CR) of the given type.

    95 |

    For the CRDs the:

    96 |
      97 |
    • forKind field represent the name of the CRD ('s' at the end for plular)
    • 98 |
    • pluralName explicitly se the plural name of the CR
    • 99 |
    • prefix field in the annotation represents the group
    • 100 |
    • shortNames short names that can be used instead of the long version (service -> svc, namespace -> ns, etc.)
    • 101 |
    • enabled by default it's enabled, but one may want to temporarily disable/silence the operator
    • 102 |
    • as for the version, currently the v1 is created automatically, but one can also create the 103 | CRD on his own before running the operator and providing the forKind and prefix 104 | matches, operator will use the existing CRD
    • 105 |
    106 |

    Documentation 112 |

    113 |
    114 | 115 | -------------------------------------------------------------------------------- /mvnw: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | # ---------------------------------------------------------------------------- 3 | # Licensed to the Apache Software Foundation (ASF) under one 4 | # or more contributor license agreements. See the NOTICE file 5 | # distributed with this work for additional information 6 | # regarding copyright ownership. The ASF licenses this file 7 | # to you under the Apache License, Version 2.0 (the 8 | # "License"); you may not use this file except in compliance 9 | # with the License. You may obtain a copy of the License at 10 | # 11 | # http://www.apache.org/licenses/LICENSE-2.0 12 | # 13 | # Unless required by applicable law or agreed to in writing, 14 | # software distributed under the License is distributed on an 15 | # "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY 16 | # KIND, either express or implied. See the License for the 17 | # specific language governing permissions and limitations 18 | # under the License. 19 | # ---------------------------------------------------------------------------- 20 | 21 | # ---------------------------------------------------------------------------- 22 | # Maven2 Start Up Batch script 23 | # 24 | # Required ENV vars: 25 | # ------------------ 26 | # JAVA_HOME - location of a JDK home dir 27 | # 28 | # Optional ENV vars 29 | # ----------------- 30 | # M2_HOME - location of maven2's installed home dir 31 | # MAVEN_OPTS - parameters passed to the Java VM when running Maven 32 | # e.g. to debug Maven itself, use 33 | # set MAVEN_OPTS=-Xdebug -Xrunjdwp:transport=dt_socket,server=y,suspend=y,address=8000 34 | # MAVEN_SKIP_RC - flag to disable loading of mavenrc files 35 | # ---------------------------------------------------------------------------- 36 | 37 | if [ -z "$MAVEN_SKIP_RC" ] ; then 38 | 39 | if [ -f /etc/mavenrc ] ; then 40 | . /etc/mavenrc 41 | fi 42 | 43 | if [ -f "$HOME/.mavenrc" ] ; then 44 | . "$HOME/.mavenrc" 45 | fi 46 | 47 | fi 48 | 49 | # OS specific support. $var _must_ be set to either true or false. 50 | cygwin=false; 51 | darwin=false; 52 | mingw=false 53 | case "`uname`" in 54 | CYGWIN*) cygwin=true ;; 55 | MINGW*) mingw=true;; 56 | Darwin*) darwin=true 57 | # Use /usr/libexec/java_home if available, otherwise fall back to /Library/Java/Home 58 | # See https://developer.apple.com/library/mac/qa/qa1170/_index.html 59 | if [ -z "$JAVA_HOME" ]; then 60 | if [ -x "/usr/libexec/java_home" ]; then 61 | export JAVA_HOME="`/usr/libexec/java_home`" 62 | else 63 | export JAVA_HOME="/Library/Java/Home" 64 | fi 65 | fi 66 | ;; 67 | esac 68 | 69 | if [ -z "$JAVA_HOME" ] ; then 70 | if [ -r /etc/gentoo-release ] ; then 71 | JAVA_HOME=`java-config --jre-home` 72 | fi 73 | fi 74 | 75 | if [ -z "$M2_HOME" ] ; then 76 | ## resolve links - $0 may be a link to maven's home 77 | PRG="$0" 78 | 79 | # need this for relative symlinks 80 | while [ -h "$PRG" ] ; do 81 | ls=`ls -ld "$PRG"` 82 | link=`expr "$ls" : '.*-> \(.*\)$'` 83 | if expr "$link" : '/.*' > /dev/null; then 84 | PRG="$link" 85 | else 86 | PRG="`dirname "$PRG"`/$link" 87 | fi 88 | done 89 | 90 | saveddir=`pwd` 91 | 92 | M2_HOME=`dirname "$PRG"`/.. 93 | 94 | # make it fully qualified 95 | M2_HOME=`cd "$M2_HOME" && pwd` 96 | 97 | cd "$saveddir" 98 | # echo Using m2 at $M2_HOME 99 | fi 100 | 101 | # For Cygwin, ensure paths are in UNIX format before anything is touched 102 | if $cygwin ; then 103 | [ -n "$M2_HOME" ] && 104 | M2_HOME=`cygpath --unix "$M2_HOME"` 105 | [ -n "$JAVA_HOME" ] && 106 | JAVA_HOME=`cygpath --unix "$JAVA_HOME"` 107 | [ -n "$CLASSPATH" ] && 108 | CLASSPATH=`cygpath --path --unix "$CLASSPATH"` 109 | fi 110 | 111 | # For Mingw, ensure paths are in UNIX format before anything is touched 112 | if $mingw ; then 113 | [ -n "$M2_HOME" ] && 114 | M2_HOME="`(cd "$M2_HOME"; pwd)`" 115 | [ -n "$JAVA_HOME" ] && 116 | JAVA_HOME="`(cd "$JAVA_HOME"; pwd)`" 117 | # TODO classpath? 118 | fi 119 | 120 | if [ -z "$JAVA_HOME" ]; then 121 | javaExecutable="`which javac`" 122 | if [ -n "$javaExecutable" ] && ! [ "`expr \"$javaExecutable\" : '\([^ ]*\)'`" = "no" ]; then 123 | # readlink(1) is not available as standard on Solaris 10. 124 | readLink=`which readlink` 125 | if [ ! `expr "$readLink" : '\([^ ]*\)'` = "no" ]; then 126 | if $darwin ; then 127 | javaHome="`dirname \"$javaExecutable\"`" 128 | javaExecutable="`cd \"$javaHome\" && pwd -P`/javac" 129 | else 130 | javaExecutable="`readlink -f \"$javaExecutable\"`" 131 | fi 132 | javaHome="`dirname \"$javaExecutable\"`" 133 | javaHome=`expr "$javaHome" : '\(.*\)/bin'` 134 | JAVA_HOME="$javaHome" 135 | export JAVA_HOME 136 | fi 137 | fi 138 | fi 139 | 140 | if [ -z "$JAVACMD" ] ; then 141 | if [ -n "$JAVA_HOME" ] ; then 142 | if [ -x "$JAVA_HOME/jre/sh/java" ] ; then 143 | # IBM's JDK on AIX uses strange locations for the executables 144 | JAVACMD="$JAVA_HOME/jre/sh/java" 145 | else 146 | JAVACMD="$JAVA_HOME/bin/java" 147 | fi 148 | else 149 | JAVACMD="`which java`" 150 | fi 151 | fi 152 | 153 | if [ ! -x "$JAVACMD" ] ; then 154 | echo "Error: JAVA_HOME is not defined correctly." >&2 155 | echo " We cannot execute $JAVACMD" >&2 156 | exit 1 157 | fi 158 | 159 | if [ -z "$JAVA_HOME" ] ; then 160 | echo "Warning: JAVA_HOME environment variable is not set." 161 | fi 162 | 163 | CLASSWORLDS_LAUNCHER=org.codehaus.plexus.classworlds.launcher.Launcher 164 | 165 | # traverses directory structure from process work directory to filesystem root 166 | # first directory with .mvn subdirectory is considered project base directory 167 | find_maven_basedir() { 168 | 169 | if [ -z "$1" ] 170 | then 171 | echo "Path not specified to find_maven_basedir" 172 | return 1 173 | fi 174 | 175 | basedir="$1" 176 | wdir="$1" 177 | while [ "$wdir" != '/' ] ; do 178 | if [ -d "$wdir"/.mvn ] ; then 179 | basedir=$wdir 180 | break 181 | fi 182 | # workaround for JBEAP-8937 (on Solaris 10/Sparc) 183 | if [ -d "${wdir}" ]; then 184 | wdir=`cd "$wdir/.."; pwd` 185 | fi 186 | # end of workaround 187 | done 188 | echo "${basedir}" 189 | } 190 | 191 | # concatenates all lines of a file 192 | concat_lines() { 193 | if [ -f "$1" ]; then 194 | echo "$(tr -s '\n' ' ' < "$1")" 195 | fi 196 | } 197 | 198 | BASE_DIR=`find_maven_basedir "$(pwd)"` 199 | if [ -z "$BASE_DIR" ]; then 200 | exit 1; 201 | fi 202 | 203 | ########################################################################################## 204 | # Extension to allow automatically downloading the maven-wrapper.jar from Maven-central 205 | # This allows using the maven wrapper in projects that prohibit checking in binary data. 206 | ########################################################################################## 207 | if [ -r "$BASE_DIR/.mvn/wrapper/maven-wrapper.jar" ]; then 208 | if [ "$MVNW_VERBOSE" = true ]; then 209 | echo "Found .mvn/wrapper/maven-wrapper.jar" 210 | fi 211 | else 212 | if [ "$MVNW_VERBOSE" = true ]; then 213 | echo "Couldn't find .mvn/wrapper/maven-wrapper.jar, downloading it ..." 214 | fi 215 | jarUrl="https://repo.maven.apache.org/maven2/io/takari/maven-wrapper/0.4.0/maven-wrapper-0.4.0.jar" 216 | while IFS="=" read key value; do 217 | case "$key" in (wrapperUrl) jarUrl="$value"; break ;; 218 | esac 219 | done < "$BASE_DIR/.mvn/wrapper/maven-wrapper.properties" 220 | if [ "$MVNW_VERBOSE" = true ]; then 221 | echo "Downloading from: $jarUrl" 222 | fi 223 | wrapperJarPath="$BASE_DIR/.mvn/wrapper/maven-wrapper.jar" 224 | 225 | if command -v wget > /dev/null; then 226 | if [ "$MVNW_VERBOSE" = true ]; then 227 | echo "Found wget ... using wget" 228 | fi 229 | wget "$jarUrl" -O "$wrapperJarPath" 230 | elif command -v curl > /dev/null; then 231 | if [ "$MVNW_VERBOSE" = true ]; then 232 | echo "Found curl ... using curl" 233 | fi 234 | curl -o "$wrapperJarPath" "$jarUrl" 235 | else 236 | if [ "$MVNW_VERBOSE" = true ]; then 237 | echo "Falling back to using Java to download" 238 | fi 239 | javaClass="$BASE_DIR/.mvn/wrapper/MavenWrapperDownloader.java" 240 | if [ -e "$javaClass" ]; then 241 | if [ ! -e "$BASE_DIR/.mvn/wrapper/MavenWrapperDownloader.class" ]; then 242 | if [ "$MVNW_VERBOSE" = true ]; then 243 | echo " - Compiling MavenWrapperDownloader.java ..." 244 | fi 245 | # Compiling the Java class 246 | ("$JAVA_HOME/bin/javac" "$javaClass") 247 | fi 248 | if [ -e "$BASE_DIR/.mvn/wrapper/MavenWrapperDownloader.class" ]; then 249 | # Running the downloader 250 | if [ "$MVNW_VERBOSE" = true ]; then 251 | echo " - Running MavenWrapperDownloader.java ..." 252 | fi 253 | ("$JAVA_HOME/bin/java" -cp .mvn/wrapper MavenWrapperDownloader "$MAVEN_PROJECTBASEDIR") 254 | fi 255 | fi 256 | fi 257 | fi 258 | ########################################################################################## 259 | # End of extension 260 | ########################################################################################## 261 | 262 | export MAVEN_PROJECTBASEDIR=${MAVEN_BASEDIR:-"$BASE_DIR"} 263 | if [ "$MVNW_VERBOSE" = true ]; then 264 | echo $MAVEN_PROJECTBASEDIR 265 | fi 266 | MAVEN_OPTS="$(concat_lines "$MAVEN_PROJECTBASEDIR/.mvn/jvm.config") $MAVEN_OPTS" 267 | 268 | # For Cygwin, switch paths to Windows format before running java 269 | if $cygwin; then 270 | [ -n "$M2_HOME" ] && 271 | M2_HOME=`cygpath --path --windows "$M2_HOME"` 272 | [ -n "$JAVA_HOME" ] && 273 | JAVA_HOME=`cygpath --path --windows "$JAVA_HOME"` 274 | [ -n "$CLASSPATH" ] && 275 | CLASSPATH=`cygpath --path --windows "$CLASSPATH"` 276 | [ -n "$MAVEN_PROJECTBASEDIR" ] && 277 | MAVEN_PROJECTBASEDIR=`cygpath --path --windows "$MAVEN_PROJECTBASEDIR"` 278 | fi 279 | 280 | WRAPPER_LAUNCHER=org.apache.maven.wrapper.MavenWrapperMain 281 | 282 | exec "$JAVACMD" \ 283 | $MAVEN_OPTS \ 284 | -classpath "$MAVEN_PROJECTBASEDIR/.mvn/wrapper/maven-wrapper.jar" \ 285 | "-Dmaven.home=${M2_HOME}" "-Dmaven.multiModuleProjectDirectory=${MAVEN_PROJECTBASEDIR}" \ 286 | ${WRAPPER_LAUNCHER} $MAVEN_CONFIG "$@" 287 | -------------------------------------------------------------------------------- /src/main/java/io/radanalytics/operator/common/AbstractWatcher.java: -------------------------------------------------------------------------------- 1 | package io.radanalytics.operator.common; 2 | 3 | import io.fabric8.kubernetes.api.model.ConfigMap; 4 | import io.fabric8.kubernetes.api.model.ConfigMapList; 5 | import io.fabric8.kubernetes.api.model.DoneableConfigMap; 6 | import io.fabric8.kubernetes.api.model.apiextensions.CustomResourceDefinition; 7 | import io.fabric8.kubernetes.client.KubernetesClient; 8 | import io.fabric8.kubernetes.client.KubernetesClientException; 9 | import io.fabric8.kubernetes.client.Watch; 10 | import io.fabric8.kubernetes.client.Watcher; 11 | import io.fabric8.kubernetes.client.dsl.MixedOperation; 12 | import io.fabric8.kubernetes.client.dsl.Resource; 13 | import io.fabric8.kubernetes.client.dsl.Watchable; 14 | import io.radanalytics.operator.SDKEntrypoint; 15 | import io.radanalytics.operator.common.crd.InfoClass; 16 | import io.radanalytics.operator.common.crd.InfoClassDoneable; 17 | import io.radanalytics.operator.common.crd.InfoList; 18 | import org.slf4j.Logger; 19 | import org.slf4j.LoggerFactory; 20 | 21 | import java.util.Map; 22 | import java.util.concurrent.CompletableFuture; 23 | import java.util.function.BiConsumer; 24 | import java.util.function.Function; 25 | import java.util.function.Predicate; 26 | 27 | import static io.radanalytics.operator.common.AnsiColors.*; 28 | 29 | public abstract class AbstractWatcher { 30 | 31 | protected static final Logger log = LoggerFactory.getLogger(AbstractWatcher.class.getName()); 32 | 33 | private final boolean isCrd; 34 | private final String namespace; 35 | private final String entityName; 36 | private final KubernetesClient client; 37 | private final CustomResourceDefinition crd; 38 | private final Map selector; 39 | 40 | private final BiConsumer onAdd; 41 | private final BiConsumer onDelete; 42 | private final BiConsumer onModify; 43 | 44 | private final Predicate isSupported; 45 | private final Function convert; 46 | private final Function convertCr; 47 | 48 | private volatile Watch watch; 49 | protected volatile boolean fullReconciliationRun = false; 50 | 51 | // use via builder 52 | protected AbstractWatcher(boolean isCrd, String namespace, String entityName, KubernetesClient client, 53 | CustomResourceDefinition crd, Map selector, BiConsumer onAdd, 54 | BiConsumer onDelete, BiConsumer onModify, Predicate isSupported, 55 | Function convert, Function convertCr) { 56 | this.isCrd = isCrd; 57 | this.namespace = namespace; 58 | this.entityName = entityName; 59 | this.client = client; 60 | this.crd = crd; 61 | this.selector = selector; 62 | this.onAdd = onAdd; 63 | this.onDelete = onDelete; 64 | this.onModify = onModify; 65 | this.isSupported = isSupported; 66 | this.convert = convert; 67 | this.convertCr = convertCr; 68 | } 69 | 70 | public abstract CompletableFuture> watch(); 71 | 72 | protected CompletableFuture createConfigMapWatch() { 73 | CompletableFuture cf = CompletableFuture.supplyAsync(() -> { 74 | MixedOperation> aux = client.configMaps(); 75 | 76 | final boolean inAllNs = "*".equals(namespace); 77 | Watchable> watchable = inAllNs ? aux.inAnyNamespace().withLabels(selector) : 78 | aux.inNamespace(namespace).withLabels(selector); 79 | Watch watch = watchable.watch(new Watcher() { 80 | @Override 81 | public void eventReceived(Action action, ConfigMap cm) { 82 | if (isSupported.test(cm)) { 83 | log.info("ConfigMap in namespace {} was {}\nCM:\n{}\n", namespace, action, cm); 84 | T entity = convert.apply(cm); 85 | if (entity == null) { 86 | log.error("something went wrong, unable to parse {} definition", entityName); 87 | } 88 | if (action.equals(Action.ERROR)) { 89 | log.error("Failed ConfigMap {} in namespace{} ", cm, namespace); 90 | } else { 91 | handleAction(action, entity, inAllNs ? cm.getMetadata().getNamespace() : namespace); 92 | } 93 | } else { 94 | log.error("Unknown CM kind: {}", cm.toString()); 95 | } 96 | } 97 | 98 | @Override 99 | public void onClose(KubernetesClientException e) { 100 | if (e != null) { 101 | log.error("Watcher closed with exception in namespace {}", namespace, e); 102 | recreateWatcher(); 103 | } else { 104 | log.info("Watcher closed in namespace {}", namespace); 105 | } 106 | } 107 | }); 108 | return watch; 109 | }, SDKEntrypoint.getExecutors()); 110 | cf.thenApply(w -> { 111 | log.info("ConfigMap watcher running for labels {}", selector); 112 | return w; 113 | }).exceptionally(e -> { 114 | log.error("ConfigMap watcher failed to start", e.getCause()); 115 | return null; 116 | }); 117 | return cf; 118 | } 119 | 120 | protected CompletableFuture createCustomResourceWatch() { 121 | CompletableFuture cf = CompletableFuture.supplyAsync(() -> { 122 | MixedOperation> aux = 123 | client.customResources(crd, InfoClass.class, InfoList.class, InfoClassDoneable.class); 124 | 125 | final boolean inAllNs = "*".equals(namespace); 126 | Watchable> watchable = inAllNs ? aux.inAnyNamespace() : aux.inNamespace(namespace); 127 | Watch watch = watchable.watch(new Watcher() { 128 | @Override 129 | public void eventReceived(Action action, InfoClass info) { 130 | log.info("Custom resource in namespace {} was {}\nCR:\n{}", namespace, action, info); 131 | T entity = convertCr.apply(info); 132 | if (entity == null) { 133 | log.error("something went wrong, unable to parse {} definition", entityName); 134 | } 135 | if (action.equals(Action.ERROR)) { 136 | log.error("Failed Custom resource {} in namespace{} ", info, namespace); 137 | } else { 138 | handleAction(action, entity, inAllNs ? info.getMetadata().getNamespace() : namespace); 139 | } 140 | } 141 | 142 | @Override 143 | public void onClose(KubernetesClientException e) { 144 | if (e != null) { 145 | log.error("Watcher closed with exception in namespace {}", namespace, e); 146 | recreateWatcher(); 147 | } else { 148 | log.info("Watcher closed in namespace {}", namespace); 149 | } 150 | } 151 | }); 152 | AbstractWatcher.this.watch = watch; 153 | return watch; 154 | }, SDKEntrypoint.getExecutors()); 155 | cf.thenApply(w -> { 156 | log.info("CustomResource watcher running for kinds {}", entityName); 157 | return w; 158 | }).exceptionally(e -> { 159 | log.error("CustomResource watcher failed to start", e.getCause()); 160 | return null; 161 | }); 162 | return cf; 163 | } 164 | 165 | private void recreateWatcher() { 166 | this.watch.close(); 167 | CompletableFuture configMapWatch = isCrd ? createCustomResourceWatch() : createConfigMapWatch(); 168 | final String crdOrCm = isCrd ? "CustomResource" : "ConfigMap"; 169 | configMapWatch.thenApply(res -> { 170 | log.info("{} watch recreated in namespace {}", crdOrCm, namespace); 171 | this.watch = res; 172 | return res; 173 | }).exceptionally(e -> { 174 | log.error("Failed to recreate {} watch in namespace {}", crdOrCm, namespace); 175 | return null; 176 | }); 177 | } 178 | 179 | private void handleAction(Watcher.Action action, T entity, String ns) { 180 | if (!fullReconciliationRun) { 181 | return; 182 | } 183 | String name = entity.getName(); 184 | try { 185 | switch (action) { 186 | case ADDED: 187 | log.info("{}creating{} {}: \n{}\n", gr(), xx(), entityName, name); 188 | onAdd.accept(entity, ns); 189 | log.info("{} {} has been {}created{}", entityName, name, gr(), xx()); 190 | break; 191 | case DELETED: 192 | log.info("{}deleting{} {}: \n{}\n", gr(), xx(), entityName, name); 193 | onDelete.accept(entity, ns); 194 | log.info("{} {} has been {}deleted{}", entityName, name, gr(), xx()); 195 | break; 196 | case MODIFIED: 197 | log.info("{}modifying{} {}: \n{}\n", gr(), xx(), entityName, name); 198 | onModify.accept(entity, ns); 199 | log.info("{} {} has been {}modified{}", entityName, name, gr(), xx()); 200 | break; 201 | default: 202 | log.error("Unknown action: {} in namespace {}", action, namespace); 203 | } 204 | } catch (Exception e) { 205 | log.warn("{}Error{} when reacting on event, cause: {}", re(), xx(), e.getMessage()); 206 | e.printStackTrace(); 207 | } 208 | } 209 | 210 | public void close() { 211 | log.info("Stopping {} for namespace {}", isCrd ? "CustomResourceWatch" : "ConfigMapWatch", namespace); 212 | watch.close(); 213 | client.close(); 214 | } 215 | 216 | public void setFullReconciliationRun(boolean fullReconciliationRun) { 217 | this.fullReconciliationRun = fullReconciliationRun; 218 | } 219 | } 220 | 221 | 222 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Apache License 2 | Version 2.0, January 2004 3 | http://www.apache.org/licenses/ 4 | 5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 6 | 7 | 1. Definitions. 8 | 9 | "License" shall mean the terms and conditions for use, reproduction, 10 | and distribution as defined by Sections 1 through 9 of this document. 11 | 12 | "Licensor" shall mean the copyright owner or entity authorized by 13 | the copyright owner that is granting the License. 14 | 15 | "Legal Entity" shall mean the union of the acting entity and all 16 | other entities that control, are controlled by, or are under common 17 | control with that entity. For the purposes of this definition, 18 | "control" means (i) the power, direct or indirect, to cause the 19 | direction or management of such entity, whether by contract or 20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 21 | outstanding shares, or (iii) beneficial ownership of such entity. 22 | 23 | "You" (or "Your") shall mean an individual or Legal Entity 24 | exercising permissions granted by this License. 25 | 26 | "Source" form shall mean the preferred form for making modifications, 27 | including but not limited to software source code, documentation 28 | source, and configuration files. 29 | 30 | "Object" form shall mean any form resulting from mechanical 31 | transformation or translation of a Source form, including but 32 | not limited to compiled object code, generated documentation, 33 | and conversions to other media types. 34 | 35 | "Work" shall mean the work of authorship, whether in Source or 36 | Object form, made available under the License, as indicated by a 37 | copyright notice that is included in or attached to the work 38 | (an example is provided in the Appendix below). 39 | 40 | "Derivative Works" shall mean any work, whether in Source or Object 41 | form, that is based on (or derived from) the Work and for which the 42 | editorial revisions, annotations, elaborations, or other modifications 43 | represent, as a whole, an original work of authorship. For the purposes 44 | of this License, Derivative Works shall not include works that remain 45 | separable from, or merely link (or bind by name) to the interfaces of, 46 | the Work and Derivative Works thereof. 47 | 48 | "Contribution" shall mean any work of authorship, including 49 | the original version of the Work and any modifications or additions 50 | to that Work or Derivative Works thereof, that is intentionally 51 | submitted to Licensor for inclusion in the Work by the copyright owner 52 | or by an individual or Legal Entity authorized to submit on behalf of 53 | the copyright owner. For the purposes of this definition, "submitted" 54 | means any form of electronic, verbal, or written communication sent 55 | to the Licensor or its representatives, including but not limited to 56 | communication on electronic mailing lists, source code control systems, 57 | and issue tracking systems that are managed by, or on behalf of, the 58 | Licensor for the purpose of discussing and improving the Work, but 59 | excluding communication that is conspicuously marked or otherwise 60 | designated in writing by the copyright owner as "Not a Contribution." 61 | 62 | "Contributor" shall mean Licensor and any individual or Legal Entity 63 | on behalf of whom a Contribution has been received by Licensor and 64 | subsequently incorporated within the Work. 65 | 66 | 2. Grant of Copyright License. Subject to the terms and conditions of 67 | this License, each Contributor hereby grants to You a perpetual, 68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 69 | copyright license to reproduce, prepare Derivative Works of, 70 | publicly display, publicly perform, sublicense, and distribute the 71 | Work and such Derivative Works in Source or Object form. 72 | 73 | 3. Grant of Patent License. Subject to the terms and conditions of 74 | this License, each Contributor hereby grants to You a perpetual, 75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 76 | (except as stated in this section) patent license to make, have made, 77 | use, offer to sell, sell, import, and otherwise transfer the Work, 78 | where such license applies only to those patent claims licensable 79 | by such Contributor that are necessarily infringed by their 80 | Contribution(s) alone or by combination of their Contribution(s) 81 | with the Work to which such Contribution(s) was submitted. If You 82 | institute patent litigation against any entity (including a 83 | cross-claim or counterclaim in a lawsuit) alleging that the Work 84 | or a Contribution incorporated within the Work constitutes direct 85 | or contributory patent infringement, then any patent licenses 86 | granted to You under this License for that Work shall terminate 87 | as of the date such litigation is filed. 88 | 89 | 4. Redistribution. You may reproduce and distribute copies of the 90 | Work or Derivative Works thereof in any medium, with or without 91 | modifications, and in Source or Object form, provided that You 92 | meet the following conditions: 93 | 94 | (a) You must give any other recipients of the Work or 95 | Derivative Works a copy of this License; and 96 | 97 | (b) You must cause any modified files to carry prominent notices 98 | stating that You changed the files; and 99 | 100 | (c) You must retain, in the Source form of any Derivative Works 101 | that You distribute, all copyright, patent, trademark, and 102 | attribution notices from the Source form of the Work, 103 | excluding those notices that do not pertain to any part of 104 | the Derivative Works; and 105 | 106 | (d) If the Work includes a "NOTICE" text file as part of its 107 | distribution, then any Derivative Works that You distribute must 108 | include a readable copy of the attribution notices contained 109 | within such NOTICE file, excluding those notices that do not 110 | pertain to any part of the Derivative Works, in at least one 111 | of the following places: within a NOTICE text file distributed 112 | as part of the Derivative Works; within the Source form or 113 | documentation, if provided along with the Derivative Works; or, 114 | within a display generated by the Derivative Works, if and 115 | wherever such third-party notices normally appear. The contents 116 | of the NOTICE file are for informational purposes only and 117 | do not modify the License. You may add Your own attribution 118 | notices within Derivative Works that You distribute, alongside 119 | or as an addendum to the NOTICE text from the Work, provided 120 | that such additional attribution notices cannot be construed 121 | as modifying the License. 122 | 123 | You may add Your own copyright statement to Your modifications and 124 | may provide additional or different license terms and conditions 125 | for use, reproduction, or distribution of Your modifications, or 126 | for any such Derivative Works as a whole, provided Your use, 127 | reproduction, and distribution of the Work otherwise complies with 128 | the conditions stated in this License. 129 | 130 | 5. Submission of Contributions. Unless You explicitly state otherwise, 131 | any Contribution intentionally submitted for inclusion in the Work 132 | by You to the Licensor shall be under the terms and conditions of 133 | this License, without any additional terms or conditions. 134 | Notwithstanding the above, nothing herein shall supersede or modify 135 | the terms of any separate license agreement you may have executed 136 | with Licensor regarding such Contributions. 137 | 138 | 6. Trademarks. This License does not grant permission to use the trade 139 | names, trademarks, service marks, or product names of the Licensor, 140 | except as required for reasonable and customary use in describing the 141 | origin of the Work and reproducing the content of the NOTICE file. 142 | 143 | 7. Disclaimer of Warranty. Unless required by applicable law or 144 | agreed to in writing, Licensor provides the Work (and each 145 | Contributor provides its Contributions) on an "AS IS" BASIS, 146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 147 | implied, including, without limitation, any warranties or conditions 148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 149 | PARTICULAR PURPOSE. You are solely responsible for determining the 150 | appropriateness of using or redistributing the Work and assume any 151 | risks associated with Your exercise of permissions under this License. 152 | 153 | 8. Limitation of Liability. In no event and under no legal theory, 154 | whether in tort (including negligence), contract, or otherwise, 155 | unless required by applicable law (such as deliberate and grossly 156 | negligent acts) or agreed to in writing, shall any Contributor be 157 | liable to You for damages, including any direct, indirect, special, 158 | incidental, or consequential damages of any character arising as a 159 | result of this License or out of the use or inability to use the 160 | Work (including but not limited to damages for loss of goodwill, 161 | work stoppage, computer failure or malfunction, or any and all 162 | other commercial damages or losses), even if such Contributor 163 | has been advised of the possibility of such damages. 164 | 165 | 9. Accepting Warranty or Additional Liability. While redistributing 166 | the Work or Derivative Works thereof, You may choose to offer, 167 | and charge a fee for, acceptance of support, warranty, indemnity, 168 | or other liability obligations and/or rights consistent with this 169 | License. However, in accepting such obligations, You may act only 170 | on Your own behalf and on Your sole responsibility, not on behalf 171 | of any other Contributor, and only if You agree to indemnify, 172 | defend, and hold each Contributor harmless for any liability 173 | incurred by, or claims asserted against, such Contributor by reason 174 | of your accepting any such warranty or additional liability. 175 | 176 | END OF TERMS AND CONDITIONS 177 | 178 | APPENDIX: How to apply the Apache License to your work. 179 | 180 | To apply the Apache License to your work, attach the following 181 | boilerplate notice, with the fields enclosed by brackets "{}" 182 | replaced with your own identifying information. (Don't include 183 | the brackets!) The text should be enclosed in the appropriate 184 | comment syntax for the file format. We also recommend that a 185 | file or class name and description of purpose be included on the 186 | same "printed page" as the copyright notice for easier 187 | identification within third-party archives. 188 | 189 | Copyright {yyyy} {name of copyright owner} 190 | 191 | Licensed under the Apache License, Version 2.0 (the "License"); 192 | you may not use this file except in compliance with the License. 193 | You may obtain a copy of the License at 194 | 195 | http://www.apache.org/licenses/LICENSE-2.0 196 | 197 | Unless required by applicable law or agreed to in writing, software 198 | distributed under the License is distributed on an "AS IS" BASIS, 199 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 200 | See the License for the specific language governing permissions and 201 | limitations under the License. 202 | -------------------------------------------------------------------------------- /src/main/java/io/radanalytics/operator/SDKEntrypoint.java: -------------------------------------------------------------------------------- 1 | package io.radanalytics.operator; 2 | 3 | import com.jcabi.manifests.Manifests; 4 | import io.fabric8.kubernetes.api.model.Service; 5 | import io.fabric8.kubernetes.api.model.ServiceBuilder; 6 | import io.fabric8.kubernetes.api.model.ServicePortBuilder; 7 | import io.fabric8.kubernetes.client.ConfigBuilder; 8 | import io.fabric8.kubernetes.client.DefaultKubernetesClient; 9 | import io.fabric8.kubernetes.client.KubernetesClient; 10 | import io.fabric8.kubernetes.client.Watch; 11 | import io.fabric8.kubernetes.client.utils.HttpClientUtils; 12 | import io.prometheus.client.Gauge; 13 | import io.prometheus.client.exporter.HTTPServer; 14 | import io.prometheus.client.hotspot.DefaultExports; 15 | import io.prometheus.client.log4j.InstrumentedAppender; 16 | import io.quarkus.runtime.ShutdownEvent; 17 | import io.quarkus.runtime.StartupEvent; 18 | import io.radanalytics.operator.common.AbstractOperator; 19 | import io.radanalytics.operator.common.AnsiColors; 20 | import io.radanalytics.operator.common.EntityInfo; 21 | import io.radanalytics.operator.common.OperatorConfig; 22 | import okhttp3.HttpUrl; 23 | import okhttp3.OkHttpClient; 24 | import okhttp3.Request; 25 | import okhttp3.Response; 26 | import org.slf4j.Logger; 27 | 28 | import javax.annotation.PostConstruct; 29 | import javax.enterprise.context.ApplicationScoped; 30 | import javax.enterprise.event.Observes; 31 | import javax.enterprise.inject.Any; 32 | import javax.enterprise.inject.Instance; 33 | import javax.inject.Inject; 34 | import javax.net.ssl.SSLContext; 35 | import javax.net.ssl.SSLSocketFactory; 36 | import javax.net.ssl.X509TrustManager; 37 | import java.io.IOException; 38 | import java.net.URL; 39 | import java.security.SecureRandom; 40 | import java.security.cert.CertificateException; 41 | import java.security.cert.X509Certificate; 42 | import java.util.*; 43 | import java.util.concurrent.*; 44 | import java.util.stream.Collectors; 45 | import java.util.stream.IntStream; 46 | 47 | import static io.radanalytics.operator.common.AnsiColors.*; 48 | import static io.radanalytics.operator.common.OperatorConfig.ALL_NAMESPACES; 49 | import static io.radanalytics.operator.common.OperatorConfig.SAME_NAMESPACE; 50 | import static java.util.concurrent.TimeUnit.SECONDS; 51 | 52 | /** 53 | * Entry point class that watches on StartupEvent and should bootstrap all the registered operators 54 | * that are present on the class path. It scans the class path for those classes that have the 55 | * {@link io.radanalytics.operator.common.Operator} annotations on them or extends the {@link AbstractOperator}. 56 | */ 57 | @ApplicationScoped 58 | public class SDKEntrypoint { 59 | private static ExecutorService executors; 60 | 61 | protected OperatorConfig config; 62 | protected KubernetesClient client; 63 | protected boolean isOpenShift; 64 | 65 | @Inject 66 | private Logger log; 67 | 68 | 69 | @Inject @Any 70 | private Instance> operators; 71 | 72 | public SDKEntrypoint() { 73 | 74 | } 75 | 76 | /* this entrypoint can be called from an environment w/o CDI */ 77 | public SDKEntrypoint(Logger log) { 78 | this.log = log; 79 | init(); 80 | } 81 | 82 | @PostConstruct 83 | void init(){ 84 | config = OperatorConfig.fromMap(System.getenv()); 85 | client = new DefaultKubernetesClient(); 86 | checkIfOnOpenshift(); 87 | } 88 | 89 | void onStop(@Observes ShutdownEvent event) { 90 | log.info("Stopped"); 91 | } 92 | 93 | public void onStart(@Observes StartupEvent event) { 94 | log.info("Starting.."); 95 | CompletableFuture future = run().exceptionally(ex -> { 96 | log.error("Unable to start operator for one or more namespaces", ex); 97 | System.exit(1); 98 | return null; 99 | }); 100 | if (config.isMetrics()) { 101 | CompletableFuture> maybeMetricServer = future.thenCompose(s -> runMetrics()); 102 | } 103 | } 104 | 105 | private CompletableFuture run() { 106 | printInfo(); 107 | if (isOpenShift) { 108 | log.info("{}OpenShift{} environment detected.", AnsiColors.ye(), AnsiColors.xx()); 109 | } else { 110 | log.info("{}Kubernetes{} environment detected.", AnsiColors.ye(), AnsiColors.xx()); 111 | } 112 | 113 | List futures = new ArrayList<>(); 114 | if (operators != null) { 115 | if (SAME_NAMESPACE.equals(config.getNamespaces().iterator().next())) { // current namespace 116 | String namespace = client.getNamespace(); 117 | CompletableFuture future = runForNamespace(isOpenShift, namespace, config.getReconciliationIntervalS(), 0); 118 | futures.add(future); 119 | } else { 120 | if (ALL_NAMESPACES.equals(config.getNamespaces().iterator().next())) { 121 | CompletableFuture future = runForNamespace(isOpenShift, ALL_NAMESPACES, config.getReconciliationIntervalS(), 0); 122 | futures.add(future); 123 | } else { 124 | Iterator ns; 125 | int i; 126 | for (ns = config.getNamespaces().iterator(), i = 0; i < config.getNamespaces().size(); i++) { 127 | CompletableFuture future = runForNamespace(isOpenShift, ns.next(), config.getReconciliationIntervalS(), i); 128 | futures.add(future); 129 | } 130 | } 131 | } 132 | } 133 | return CompletableFuture.allOf(futures.toArray(new CompletableFuture[]{})); 134 | } 135 | 136 | private CompletableFuture> runMetrics() { 137 | HTTPServer httpServer = null; 138 | try { 139 | log.info("Starting a simple HTTP server for exposing internal metrics.."); 140 | httpServer = new HTTPServer(config.getMetricsPort()); 141 | log.info("{}metrics server{} listens on port {}", AnsiColors.ye(), AnsiColors.xx(), config.getMetricsPort()); 142 | } catch (IOException e) { 143 | log.error("Can't start metrics server because of: {} ", e.getMessage()); 144 | e.printStackTrace(); 145 | } 146 | if (config.isMetricsJvm()) { 147 | DefaultExports.initialize(); 148 | } 149 | final Optional maybeServer = Optional.of(httpServer); 150 | return CompletableFuture.supplyAsync(() -> maybeServer); 151 | } 152 | 153 | private CompletableFuture runForNamespace(boolean isOpenShift, String namespace, long reconInterval, int delay) { 154 | List> operatorList = operators.stream().collect(Collectors.toList()); 155 | 156 | if (operatorList.isEmpty()) { 157 | log.warn("No suitable operators were found, make sure your class extends AbstractOperator and have @Singleton on it."); 158 | } 159 | 160 | List futures = new ArrayList<>(); 161 | final int operatorNumber = operatorList.size(); 162 | IntStream.range(0, operatorNumber).forEach(operatorIndex -> { 163 | AbstractOperator operator = operatorList.get(operatorIndex); 164 | if (!AbstractOperator.class.isAssignableFrom(operator.getClass())) { 165 | log.error("Class {} annotated with @Operator doesn't extend the AbstractOperator", operator.getClass()); 166 | return; // do not fail 167 | } 168 | 169 | if (!operator.isEnabled()) { 170 | log.info("Skipping initialization of {} operator", operator.getClass()); 171 | return; 172 | } 173 | 174 | operator.setClient(client); 175 | operator.setNamespace(namespace); 176 | operator.setOpenshift(isOpenShift); 177 | 178 | CompletableFuture future = operator.start().thenApply(res -> { 179 | log.info("{} started in namespace {}", operator.getName(), namespace); 180 | return res; 181 | }).exceptionally(ex -> { 182 | log.error("{} in namespace {} failed to start", operator.getName(), namespace, ((Throwable) ex).getCause()); 183 | System.exit(1); 184 | return null; 185 | }); 186 | 187 | ScheduledExecutorService s = Executors.newScheduledThreadPool(1); 188 | int realDelay = (delay * operatorNumber) + operatorIndex + 2; 189 | ScheduledFuture scheduledFuture = 190 | s.scheduleAtFixedRate(() -> { 191 | try { 192 | operator.fullReconciliation(); 193 | operator.setFullReconciliationRun(true); 194 | } catch (Throwable t) { 195 | log.warn("error during full reconciliation: {}", t.getMessage()); 196 | t.printStackTrace(); 197 | } 198 | }, realDelay, reconInterval, SECONDS); 199 | log.info("full reconciliation for {} scheduled (periodically each {} seconds)", operator.getName(), reconInterval); 200 | log.info("the first full reconciliation for {} is happening in {} seconds", operator.getName(), realDelay); 201 | 202 | futures.add(future); 203 | }); 204 | return CompletableFuture.allOf(futures.toArray(new CompletableFuture[]{})); 205 | } 206 | 207 | private void checkIfOnOpenshift() { 208 | try { 209 | URL kubernetesApi = client.getMasterUrl(); 210 | 211 | HttpUrl.Builder urlBuilder = new HttpUrl.Builder(); 212 | urlBuilder.host(kubernetesApi.getHost()); 213 | 214 | if (kubernetesApi.getPort() == -1) { 215 | urlBuilder.port(kubernetesApi.getDefaultPort()); 216 | } else { 217 | urlBuilder.port(kubernetesApi.getPort()); 218 | } 219 | if (kubernetesApi.getProtocol().equals("https")) { 220 | urlBuilder.scheme("https"); 221 | } 222 | urlBuilder.addPathSegment("apis/route.openshift.io/v1"); 223 | 224 | OkHttpClient httpClient = HttpClientUtils.createHttpClient(new ConfigBuilder().build()); 225 | HttpUrl url = urlBuilder.build(); 226 | Response response = httpClient.newCall(new Request.Builder().url(url).build()).execute(); 227 | boolean success = response.isSuccessful(); 228 | if (success) { 229 | log.info("{} returned {}. We are on OpenShift.", url, response.code()); 230 | } else { 231 | log.info("{} returned {}. We are not on OpenShift. Assuming, we are on Kubernetes.", url, response.code()); 232 | } 233 | isOpenShift = success; 234 | } catch (Exception e) { 235 | e.printStackTrace(); 236 | log.error("Failed to distinguish between Kubernetes and OpenShift"); 237 | log.warn("Let's assume we are on K8s"); 238 | isOpenShift = false; 239 | } 240 | } 241 | 242 | private void printInfo() { 243 | String gitSha = "unknown"; 244 | String version = "unknown"; 245 | try { 246 | version = Optional.ofNullable(SDKEntrypoint.class.getPackage().getImplementationVersion()).orElse(version); 247 | gitSha = Optional.ofNullable(Manifests.read("Implementation-Build")).orElse(gitSha); 248 | } catch (Exception e) { 249 | // ignore, not critical 250 | } 251 | 252 | if(config.isMetrics()) { 253 | registerMetrics(gitSha, version); 254 | } 255 | 256 | log.info("\n{}Operator{} has started in version {}{}{}.\n", re(), xx(), gr(), 257 | version, xx()); 258 | if (!gitSha.isEmpty()) { 259 | log.info("Git sha: {}{}{}", ye(), gitSha, xx()); 260 | } 261 | log.info("==================\n"); 262 | } 263 | 264 | private void registerMetrics(String gitSha, String version) { 265 | List labels = new ArrayList<>(); 266 | List values = new ArrayList<>(); 267 | 268 | labels.addAll(Arrays.asList("gitSha", "version", 269 | "CRD", 270 | "COLORS", 271 | OperatorConfig.WATCH_NAMESPACE, 272 | OperatorConfig.METRICS, 273 | OperatorConfig.METRICS_JVM, 274 | OperatorConfig.METRICS_PORT, 275 | OperatorConfig.FULL_RECONCILIATION_INTERVAL_S, 276 | OperatorConfig.OPERATOR_OPERATION_TIMEOUT_MS 277 | )); 278 | values.addAll(Arrays.asList(gitSha, version, 279 | Optional.ofNullable(System.getenv().get("CRD")).orElse("true"), 280 | Optional.ofNullable(System.getenv().get("COLORS")).orElse("true"), 281 | SAME_NAMESPACE.equals(config.getNamespaces().iterator().next()) ? client.getNamespace() : config.getNamespaces().toString(), 282 | String.valueOf(config.isMetrics()), 283 | String.valueOf(config.isMetricsJvm()), 284 | String.valueOf(config.getMetricsPort()), 285 | String.valueOf(config.getReconciliationIntervalS()), 286 | String.valueOf(config.getOperationTimeoutMs()) 287 | )); 288 | 289 | Gauge.build() 290 | .name("operator_info") 291 | .help("Basic information about the abstract operator library.") 292 | .labelNames(labels.toArray(new String[]{})) 293 | .register() 294 | .labels(values.toArray(new String[]{})) 295 | .set(1); 296 | 297 | // add log appender for metrics 298 | final org.apache.log4j.Logger rootLogger = org.apache.log4j.Logger.getRootLogger(); 299 | InstrumentedAppender metricsLogAppender = new InstrumentedAppender(); 300 | metricsLogAppender.setName("metrics"); 301 | rootLogger.addAppender(metricsLogAppender); 302 | } 303 | 304 | public static ExecutorService getExecutors() { 305 | if (null == executors) { 306 | executors = Executors.newFixedThreadPool(10); 307 | } 308 | return executors; 309 | } 310 | 311 | public boolean isOpenShift() { 312 | return isOpenShift; 313 | } 314 | 315 | public OperatorConfig getConfig() { 316 | return config; 317 | } 318 | 319 | public KubernetesClient getClient() { 320 | return client; 321 | } 322 | } 323 | -------------------------------------------------------------------------------- /src/main/java/io/radanalytics/operator/common/AbstractOperator.java: -------------------------------------------------------------------------------- 1 | package io.radanalytics.operator.common; 2 | 3 | import io.fabric8.kubernetes.api.model.ConfigMap; 4 | import io.fabric8.kubernetes.api.model.ConfigMapList; 5 | import io.fabric8.kubernetes.api.model.DoneableConfigMap; 6 | import io.fabric8.kubernetes.api.model.apiextensions.CustomResourceDefinition; 7 | import io.fabric8.kubernetes.client.*; 8 | import io.fabric8.kubernetes.client.dsl.FilterWatchListMultiDeletable; 9 | import io.fabric8.kubernetes.client.dsl.MixedOperation; 10 | import io.fabric8.kubernetes.client.dsl.Resource; 11 | import io.radanalytics.operator.common.crd.CrdDeployer; 12 | import io.radanalytics.operator.common.crd.InfoClass; 13 | import io.radanalytics.operator.common.crd.InfoStatus; 14 | import io.radanalytics.operator.common.crd.InfoClassDoneable; 15 | import io.radanalytics.operator.common.crd.InfoList; 16 | import io.radanalytics.operator.resource.LabelsHelper; 17 | import org.slf4j.Logger; 18 | 19 | import java.util.Date; 20 | import javax.inject.Inject; 21 | import java.util.List; 22 | import java.util.Map; 23 | import java.util.Optional; 24 | import java.util.Set; 25 | import java.util.concurrent.CompletableFuture; 26 | import java.util.function.Consumer; 27 | import java.util.stream.Collectors; 28 | import java.util.stream.Stream; 29 | 30 | import static io.radanalytics.operator.common.OperatorConfig.ALL_NAMESPACES; 31 | 32 | /** 33 | * This abstract class represents the extension point of the abstract-operator library. 34 | * By extending this class and overriding the methods, you will be able to watch on the 35 | * config maps or custom resources you are interested in and handle the life-cycle of your 36 | * objects accordingly. 37 | * 38 | * Don't forget to add the @Operator annotation of the children classes. 39 | * 40 | * @param entity info class that captures the configuration of the objects we are watching 41 | */ 42 | public abstract class AbstractOperator { 43 | 44 | @Inject 45 | protected Logger log; 46 | 47 | @Inject 48 | private CrdDeployer crdDeployer; 49 | 50 | // client, isOpenshift and namespace are being set in the SDKEntrypoint from the context 51 | protected KubernetesClient client; 52 | protected boolean isOpenshift; 53 | protected String namespace; 54 | 55 | // these fields can be directly set from languages that don't support annotations, like JS 56 | protected String entityName; 57 | protected String prefix; 58 | protected String[] shortNames; 59 | protected String pluralName; 60 | protected Class infoClass; 61 | protected boolean isCrd; 62 | protected boolean enabled = true; 63 | protected String named; 64 | protected String[] additionalPrinterColumnNames; 65 | protected String[] additionalPrinterColumnPaths; 66 | protected String[] additionalPrinterColumnTypes; 67 | 68 | protected volatile boolean fullReconciliationRun = false; 69 | 70 | private Map selector; 71 | private String operatorName; 72 | private CustomResourceDefinition crd; 73 | 74 | private volatile AbstractWatcher watch; 75 | 76 | public AbstractOperator() { 77 | Operator annotation = getClass().getAnnotation(Operator.class); 78 | if (annotation != null) { 79 | this.infoClass = (Class) annotation.forKind(); 80 | this.named = annotation.named(); 81 | this.isCrd = annotation.crd(); 82 | this.enabled = annotation.enabled(); 83 | this.prefix = annotation.prefix(); 84 | this.shortNames = annotation.shortNames(); 85 | this.pluralName = annotation.pluralName(); 86 | this.additionalPrinterColumnNames = annotation.additionalPrinterColumnNames(); 87 | this.additionalPrinterColumnPaths = annotation.additionalPrinterColumnPaths(); 88 | this.additionalPrinterColumnTypes = annotation.additionalPrinterColumnTypes(); 89 | } else { 90 | log.info("Annotation on the operator class not found, falling back to direct field access."); 91 | log.info("If the initialization fails, it's probably due to the fact that some compulsory fields are missing."); 92 | log.info("Compulsory fields: infoClass"); 93 | } 94 | } 95 | 96 | /** 97 | * In this method, the user of the abstract-operator is assumed to handle the creation of 98 | * a new entity of type T. This method is called when the config map or custom resource with given 99 | * type is created. 100 | * The common use-case would be creating some new resources in the 101 | * Kubernetes cluster (using @see this.client), like replication controllers with pod specifications 102 | * and custom images and settings. But one can do arbitrary work here, like calling external APIs, etc. 103 | * 104 | * @param entity entity that represents the config map (or CR) that has just been created. 105 | * The type of the entity is passed as a type parameter to this class. 106 | */ 107 | abstract protected void onAdd(T entity); 108 | 109 | /** 110 | * Override this method if you want to manually handle the case when it watches for the events in the all 111 | * namespaces (WATCH_NAMESPACE="*"). 112 | * 113 | * 114 | * @param entity entity that represents the config map (or CR) that has just been created. 115 | * * The type of the entity is passed as a type parameter to this class. 116 | * @param namespace namespace in which the resources should be created. 117 | */ 118 | protected void onAdd(T entity, String namespace) { 119 | onAction(entity, namespace, this::onAdd); 120 | } 121 | 122 | /** 123 | * This method should handle the deletion of the resource that was represented by the config map or custom resource. 124 | * The method is called when the corresponding config map or custom resource is deleted in the Kubernetes cluster. 125 | * Some suggestion what to do here would be: cleaning the resources, deleting some resources in K8s, etc. 126 | * 127 | * @param entity entity that represents the config map or custom resource that has just been created. 128 | * The type of the entity is passed as a type parameter to this class. 129 | */ 130 | abstract protected void onDelete(T entity); 131 | 132 | 133 | /** 134 | * Override this method if you want to manually handle the case when it watches for the events in the all 135 | * namespaces (WATCH_NAMESPACE="*"). 136 | * 137 | * 138 | * @param entity entity that represents the config map (or CR) that has just been created. 139 | * * The type of the entity is passed as a type parameter to this class. 140 | * @param namespace namespace in which the resources should be created. 141 | */ 142 | protected void onDelete(T entity, String namespace) { 143 | onAction(entity, namespace, this::onDelete); 144 | } 145 | 146 | /** 147 | * It's called when one modifies the configmap of type 'T' (that passes isSupported check) or custom resource. 148 | * If this method is not overriden, the implicit behavior is calling onDelete and onAdd. 149 | * 150 | * @param entity entity that represents the config map or custom resource that has just been created. 151 | * The type of the entity is passed as a type parameter to this class. 152 | */ 153 | protected void onModify(T entity) { 154 | onDelete(entity); 155 | onAdd(entity); 156 | } 157 | 158 | /** 159 | * Override this method if you want to manually handle the case when it watches for the events in the all 160 | * namespaces (WATCH_NAMESPACE="*"). 161 | * 162 | * 163 | * @param entity entity that represents the config map (or CR) that has just been created. 164 | * * The type of the entity is passed as a type parameter to this class. 165 | * @param namespace namespace in which the resources should be created. 166 | */ 167 | protected void onModify(T entity, String namespace) { 168 | onAction(entity, namespace, this::onModify); 169 | } 170 | 171 | private void onAction(T entity, String namespace, Consumer handler) { 172 | if (ALL_NAMESPACES.equals(this.namespace)) { 173 | //synchronized (this.watch) { // events from the watch should be serialized (1 thread) 174 | try { 175 | this.namespace = namespace; 176 | handler.accept(entity); 177 | } finally { 178 | this.namespace = ALL_NAMESPACES; 179 | } 180 | //} 181 | } else { 182 | handler.accept(entity); 183 | } 184 | } 185 | 186 | /** 187 | * Override this method to do arbitrary work before the operator starts listening on configmaps or custom resources. 188 | */ 189 | protected void onInit() { 190 | // no-op by default 191 | } 192 | 193 | /** 194 | * Override this method to do a full reconciliation. 195 | */ 196 | public void fullReconciliation() { 197 | // no-op by default 198 | } 199 | 200 | /** 201 | * Implicitly only those configmaps with given prefix and kind are being watched, but you can provide additional 202 | * 'deep' checking in here. 203 | * 204 | * @param cm ConfigMap that is about to be checked 205 | * @return true if cm is the configmap we are interested in 206 | */ 207 | protected boolean isSupported(ConfigMap cm) { 208 | return true; 209 | } 210 | 211 | /** 212 | * If true, start the watcher for this operator. Otherwise it's considered as disabled. 213 | * 214 | * @return enabled 215 | */ 216 | public boolean isEnabled() { 217 | return this.enabled; 218 | } 219 | 220 | /** 221 | * Converts the configmap representation into T. 222 | * Normally, you may want to call something like: 223 | * 224 | * HasDataHelper.parseCM(FooBar.class, cm); in this method, where FooBar is of type T. 225 | * This would parse the yaml representation of the configmap's config section and creates an object of type T. 226 | * 227 | * @param cm ConfigMap that is about to be converted to T 228 | * @return entity of type T 229 | */ 230 | protected T convert(ConfigMap cm) { 231 | return ConfigMapWatcher.defaultConvert(infoClass, cm); 232 | } 233 | 234 | protected T convertCr(InfoClass info) { 235 | return CustomResourceWatcher.defaultConvert(infoClass, info); 236 | } 237 | 238 | public String getName() { 239 | return operatorName; 240 | } 241 | 242 | /** 243 | * Starts the operator and creates the watch 244 | * @return CompletableFuture 245 | */ 246 | public CompletableFuture start() { 247 | initInternals(); 248 | this.selector = LabelsHelper.forKind(entityName, prefix); 249 | boolean ok = checkIntegrity(); 250 | if (!ok) { 251 | log.warn("Unable to initialize the operator correctly, some compulsory fields are missing."); 252 | return CompletableFuture.completedFuture(null); 253 | } 254 | 255 | log.info("Starting {} for namespace {}", operatorName, namespace); 256 | 257 | if (isCrd) { 258 | this.crd = crdDeployer.initCrds(client, 259 | prefix, 260 | entityName, 261 | shortNames, 262 | pluralName, 263 | additionalPrinterColumnNames, 264 | additionalPrinterColumnPaths, 265 | additionalPrinterColumnTypes, 266 | infoClass, 267 | isOpenshift); 268 | } 269 | 270 | // onInit() can be overriden in child operators 271 | onInit(); 272 | 273 | CompletableFuture> future = initializeWatcher(); 274 | future.thenApply(res -> { 275 | this.watch = res; 276 | log.info("{}{} running{} for namespace {}", AnsiColors.gr(), operatorName, AnsiColors.xx(), 277 | Optional.ofNullable(namespace).orElse("'all'")); 278 | return res; 279 | }).exceptionally(e -> { 280 | log.error("{} startup failed for namespace {}", operatorName, namespace, e.getCause()); 281 | return null; 282 | }); 283 | return future; 284 | } 285 | 286 | private CompletableFuture> initializeWatcher() { 287 | CompletableFuture> future; 288 | if (isCrd) { 289 | CustomResourceWatcher.Builder crBuilder = new CustomResourceWatcher.Builder<>(); 290 | CustomResourceWatcher crWatcher = crBuilder.withClient(client) 291 | .withCrd(crd) 292 | .withEntityName(entityName) 293 | .withNamespace(namespace) 294 | .withConvert(this::convertCr) 295 | .withOnAdd(this::onAdd) 296 | .withOnDelete(this::onDelete) 297 | .withOnModify(this::onModify) 298 | .build(); 299 | future = crWatcher.watch(); 300 | } else { 301 | ConfigMapWatcher.Builder cmBuilder = new ConfigMapWatcher.Builder<>(); 302 | ConfigMapWatcher cmWatcher = cmBuilder.withClient(client) 303 | .withSelector(selector) 304 | .withEntityName(entityName) 305 | .withNamespace(namespace) 306 | .withConvert(this::convert) 307 | .withOnAdd(this::onAdd) 308 | .withOnDelete(this::onDelete) 309 | .withOnModify(this::onModify) 310 | .withPredicate(this::isSupported) 311 | .build(); 312 | future = cmWatcher.watch(); 313 | } 314 | return future; 315 | } 316 | 317 | private boolean checkIntegrity() { 318 | boolean ok = infoClass != null; 319 | ok = ok && entityName != null && !entityName.isEmpty(); 320 | ok = ok && prefix != null && !prefix.isEmpty() && prefix.endsWith("/"); 321 | ok = ok && operatorName != null && operatorName.endsWith("operator"); 322 | ok = ok && additionalPrinterColumnNames == null || 323 | (additionalPrinterColumnPaths != null && (additionalPrinterColumnNames.length == additionalPrinterColumnPaths.length) 324 | && (additionalPrinterColumnTypes == null || additionalPrinterColumnNames.length == additionalPrinterColumnTypes.length )); 325 | return ok; 326 | } 327 | 328 | private void initInternals() { 329 | // prefer "named" for the entity name, otherwise "entityName" and finally the converted class name. 330 | if (named != null && !named.isEmpty()) { 331 | entityName = named; 332 | } else if (entityName != null && !entityName.isEmpty()) { 333 | // ok case 334 | } else if (infoClass != null) { 335 | entityName = infoClass.getSimpleName(); 336 | } else { 337 | entityName = ""; 338 | } 339 | 340 | // if CRD env variable is defined, it will override the annotation parameter 341 | if (null != System.getenv("CRD")) { 342 | isCrd = !"false".equals(System.getenv("CRD")); 343 | } 344 | prefix = prefix == null || prefix.isEmpty() ? getClass().getPackage().getName() : prefix; 345 | prefix = prefix + (!prefix.endsWith("/") ? "/" : ""); 346 | operatorName = "'" + entityName + "' operator"; 347 | } 348 | 349 | public void stop() { 350 | log.info("Stopping {} for namespace {}", operatorName, namespace); 351 | watch.close(); 352 | client.close(); 353 | } 354 | 355 | /** 356 | * Call this method in the concrete operator to obtain the desired state of the system. This can be especially handy 357 | * during the fullReconciliation. Rule of thumb is that if you are overriding fullReconciliation, you 358 | * should also override this method and call it from fullReconciliation() to ensure that the real state 359 | * is the same as the desired state. 360 | * 361 | * @return returns the set of 'T's that correspond to the CMs or CRs that have been created in the K8s 362 | */ 363 | protected Set getDesiredSet() { 364 | Set desiredSet; 365 | if (isCrd) { 366 | MixedOperation> aux1 = 367 | client.customResources(crd, InfoClass.class, InfoList.class, InfoClassDoneable.class); 368 | FilterWatchListMultiDeletable> aux2 = 369 | "*".equals(namespace) ? aux1.inAnyNamespace() : aux1.inNamespace(namespace); 370 | CustomResourceList listAux = aux2.list(); 371 | List items = listAux.getItems(); 372 | desiredSet = items.stream().flatMap(item -> { 373 | try { 374 | return Stream.of(convertCr(item)); 375 | } catch (Exception e) { 376 | // ignore this CR 377 | return Stream.empty(); 378 | } 379 | }).collect(Collectors.toSet()); 380 | } else { 381 | MixedOperation> aux1 = 382 | client.configMaps(); 383 | FilterWatchListMultiDeletable> aux2 = 384 | "*".equals(namespace) ? aux1.inAnyNamespace() : aux1.inNamespace(namespace); 385 | desiredSet = aux2.withLabels(selector) 386 | .list() 387 | .getItems() 388 | .stream() 389 | .flatMap(item -> { 390 | try { 391 | return Stream.of(convert(item)); 392 | } catch (Exception e) { 393 | // ignore this CM 394 | return Stream.empty(); 395 | } 396 | }).collect(Collectors.toSet()); 397 | } 398 | return desiredSet; 399 | } 400 | 401 | /** 402 | * Sets the 'state' field in the status block of the CR identified by namespace and name. 403 | * The status block in the CR has another component 'lastTransitionTime' which is set 404 | * automatically. Note, this only works for custom resource watchers, it has no effect for configmap watchers. 405 | * 406 | * @param status String value that will be assigned to the 'state' field in the CR status block 407 | * @param namespace The namespace holding the CR to update 408 | * @param name The name of the CR to update 409 | **/ 410 | protected void setCRStatus(String status, String namespace, String name) { 411 | if (isCrd) { 412 | MixedOperation> crclient = 413 | client.customResources(crd, InfoClass.class, InfoList.class, InfoClassDoneable.class); 414 | 415 | InfoClass cr = crclient.inNamespace(namespace).withName(name).get(); 416 | if (cr != null) { 417 | cr.setStatus(new InfoStatus(status, new Date())); 418 | crclient.inNamespace(namespace).withName(name).updateStatus(cr); 419 | } 420 | } 421 | } 422 | 423 | public void setClient(KubernetesClient client) { 424 | this.client = client; 425 | } 426 | 427 | public void setOpenshift(boolean openshift) { 428 | isOpenshift = openshift; 429 | } 430 | 431 | public void setNamespace(String namespace) { 432 | this.namespace = namespace; 433 | } 434 | 435 | public void setEntityName(String entityName) { 436 | this.entityName = entityName; 437 | } 438 | 439 | public void setPrefix(String prefix) { 440 | this.prefix = prefix; 441 | } 442 | 443 | public void setInfoClass(Class infoClass) { 444 | this.infoClass = infoClass; 445 | } 446 | 447 | public void setCrd(boolean crd) { 448 | isCrd = crd; 449 | } 450 | 451 | public void setEnabled(boolean enabled) { 452 | this.enabled = enabled; 453 | } 454 | 455 | public void setNamed(String named) { 456 | this.named = named; 457 | } 458 | 459 | public void setFullReconciliationRun(boolean fullReconciliationRun) { 460 | this.fullReconciliationRun = fullReconciliationRun; 461 | this.watch.setFullReconciliationRun(true); 462 | } 463 | } 464 | --------------------------------------------------------------------------------