├── test ├── cleanup-args │ ├── 02-cleanup.yaml │ ├── 00-assert.yaml │ ├── 02-assert.yaml │ ├── 00-install.yaml │ ├── 01-assert.yaml │ └── 01-new-args.yaml ├── cleanup-certs │ ├── 02-cleanup.yaml │ ├── 00-assert.yaml │ ├── 02-assert.yaml │ ├── 00-install.yaml │ ├── 01-assert.yaml │ └── 01-new-certs.yaml ├── cleanup-env-vars │ ├── 02-cleanup.yaml │ ├── 00-assert.yaml │ ├── 00-install.yaml │ ├── 02-assert.yaml │ ├── 01-assert.yaml │ └── 01-new-vars.yaml ├── cleanup-image │ ├── 02-cleanup.yaml │ ├── 00-assert.yaml │ ├── 02-assert.yaml │ ├── 00-install.yaml │ ├── 01-assert.yaml │ └── 01-new-image.yaml ├── cleanup-init-args │ ├── 02-cleanup.yaml │ ├── 00-assert.yaml │ ├── 02-assert.yaml │ ├── 00-install.yaml │ ├── 01-assert.yaml │ └── 01-new-args.yaml ├── cleanup-resources │ ├── 02-cleanup.yaml │ ├── 00-assert.yaml │ ├── 02-assert.yaml │ ├── 00-install.yaml │ ├── 01-assert.yaml │ └── 01-new-resources.yaml ├── cleanup-services │ ├── 02-cleanup.yaml │ ├── 00-assert.yaml │ ├── 00-install.yaml │ ├── 02-assert.yaml │ ├── 01-assert.yaml │ └── 01-new-services.yaml ├── cleanup-init-image │ ├── 02-cleanup.yaml │ ├── 00-assert.yaml │ ├── 02-assert.yaml │ ├── 00-install.yaml │ ├── 01-assert.yaml │ └── 01-new-init-image.yaml ├── cleanup-intfmapping │ ├── 02-cleanup.yaml │ ├── 00-assert.yaml │ ├── 00-install.yaml │ ├── 02-assert.yaml │ ├── 01-assert.yaml │ └── 01-new-intfmapping.yaml ├── cleanup-waitforagents │ ├── 02-cleanup.yaml │ ├── 00-assert.yaml │ ├── 00-install.yaml │ ├── 02-assert.yaml │ ├── 01-assert.yaml │ └── 01-new-waitforagents.yaml ├── cleanup-toggleoverrides │ ├── 02-cleanup.yaml │ ├── 00-assert.yaml │ ├── 02-assert.yaml │ ├── 00-install.yaml │ ├── 01-assert.yaml │ └── 01-new-toggleoverrides.yaml ├── reconcile-args │ ├── 00-assert.yaml │ ├── 00-install.yaml │ ├── 01-new-args.yaml │ └── 01-assert.yaml ├── reconcile-certs │ ├── 00-assert.yaml │ ├── 00-install.yaml │ ├── 01-new-certs.yaml │ └── 01-assert.yaml ├── reconcile-env-vars │ ├── 00-assert.yaml │ ├── 00-install.yaml │ ├── 01-new-vars.yaml │ └── 01-assert.yaml ├── reconcile-image │ ├── 00-assert.yaml │ ├── 00-install.yaml │ ├── 01-new-image.yaml │ └── 01-assert.yaml ├── reconcile-services │ ├── 00-assert.yaml │ ├── 00-install.yaml │ ├── 01-new-services.yaml │ └── 01-assert.yaml ├── reconcile-init-args │ ├── 00-assert.yaml │ ├── 00-install.yaml │ ├── 01-new-args.yaml │ └── 01-assert.yaml ├── reconcile-init-image │ ├── 00-assert.yaml │ ├── 00-install.yaml │ ├── 01-new-init-image.yaml │ └── 01-assert.yaml ├── reconcile-intfmapping │ ├── 00-assert.yaml │ ├── 00-install.yaml │ ├── 01-new-intfmapping.yaml │ └── 01-assert.yaml ├── reconcile-resources │ ├── 00-assert.yaml │ ├── 00-install.yaml │ ├── 01-new-resources.yaml │ └── 01-assert.yaml ├── reconcile-waitforagents │ ├── 00-assert.yaml │ ├── 00-install.yaml │ ├── 01-new-waitforagents.yaml │ └── 01-assert.yaml ├── reconcile-toggleoverrides │ ├── 00-assert.yaml │ ├── 00-install.yaml │ ├── 01-new-toggleoverrides.yaml │ └── 01-assert.yaml ├── default-init │ ├── 00-install.yaml │ └── 00-assert.yaml ├── cleanup.yaml └── patch.json ├── config ├── prometheus │ ├── kustomization.yaml │ └── monitor.yaml ├── rbac │ ├── service_account.yaml │ ├── auth_proxy_client_clusterrole.yaml │ ├── role_binding.yaml │ ├── auth_proxy_role_binding.yaml │ ├── leader_election_role_binding.yaml │ ├── auth_proxy_role.yaml │ ├── auth_proxy_service.yaml │ ├── ceoslabdevice_viewer_role.yaml │ ├── ceoslabdevice_editor_role.yaml │ ├── leader_election_role.yaml │ ├── kustomization.yaml │ └── role.yaml ├── scorecard │ ├── bases │ │ └── config.yaml │ ├── patches │ │ ├── basic.config.yaml │ │ └── olm.config.yaml │ └── kustomization.yaml ├── samples │ ├── ceoslab_v1alpha1_ceoslabdevice.yaml │ └── kustomization.yaml ├── manager │ ├── controller_manager_config.yaml │ ├── kustomization.yaml │ └── manager.yaml ├── crd │ ├── patches │ │ ├── cainjection_in_ceoslabdevices.yaml │ │ └── webhook_in_ceoslabdevices.yaml │ ├── kustomizeconfig.yaml │ ├── kustomization.yaml │ └── bases │ │ └── ceoslab.arista.com_ceoslabdevices.yaml ├── default │ ├── manager_config_patch.yaml │ ├── manager_auth_proxy_patch.yaml │ └── kustomization.yaml ├── manifests │ ├── kustomization.yaml │ └── bases │ │ └── arista-ceoslab-operator.clusterserviceversion.yaml └── kustomized │ └── manifest.yaml ├── .dockerignore ├── kuttl-test.yaml ├── .gitignore ├── PROJECT ├── hack └── boilerplate.go.txt ├── Dockerfile ├── api └── v1alpha1 │ ├── dynamic │ ├── fake │ │ └── client.go │ └── client.go │ ├── client │ └── interface.go │ ├── groupversion_info.go │ ├── clientset │ └── client.go │ ├── ceoslabdevice_types.go │ └── zz_generated.deepcopy.go ├── README.md ├── controllers ├── suite_test.go └── ceoslabdevice_controller.go ├── .github └── workflows │ └── docker-publish.yml ├── main.go ├── go.mod ├── Makefile └── LICENSE /test/cleanup-args/02-cleanup.yaml: -------------------------------------------------------------------------------- 1 | ../cleanup.yaml -------------------------------------------------------------------------------- /test/cleanup-certs/02-cleanup.yaml: -------------------------------------------------------------------------------- 1 | ../cleanup.yaml -------------------------------------------------------------------------------- /test/cleanup-env-vars/02-cleanup.yaml: -------------------------------------------------------------------------------- 1 | ../cleanup.yaml -------------------------------------------------------------------------------- /test/cleanup-image/02-cleanup.yaml: -------------------------------------------------------------------------------- 1 | ../cleanup.yaml -------------------------------------------------------------------------------- /test/cleanup-init-args/02-cleanup.yaml: -------------------------------------------------------------------------------- 1 | ../cleanup.yaml -------------------------------------------------------------------------------- /test/cleanup-resources/02-cleanup.yaml: -------------------------------------------------------------------------------- 1 | ../cleanup.yaml -------------------------------------------------------------------------------- /test/cleanup-services/02-cleanup.yaml: -------------------------------------------------------------------------------- 1 | ../cleanup.yaml -------------------------------------------------------------------------------- /test/cleanup-init-image/02-cleanup.yaml: -------------------------------------------------------------------------------- 1 | ../cleanup.yaml -------------------------------------------------------------------------------- /test/cleanup-intfmapping/02-cleanup.yaml: -------------------------------------------------------------------------------- 1 | ../cleanup.yaml -------------------------------------------------------------------------------- /test/cleanup-waitforagents/02-cleanup.yaml: -------------------------------------------------------------------------------- 1 | ../cleanup.yaml -------------------------------------------------------------------------------- /test/cleanup-args/00-assert.yaml: -------------------------------------------------------------------------------- 1 | ../default-init/00-assert.yaml -------------------------------------------------------------------------------- /test/cleanup-args/02-assert.yaml: -------------------------------------------------------------------------------- 1 | ../default-init/00-assert.yaml -------------------------------------------------------------------------------- /test/cleanup-certs/00-assert.yaml: -------------------------------------------------------------------------------- 1 | ../default-init/00-assert.yaml -------------------------------------------------------------------------------- /test/cleanup-certs/02-assert.yaml: -------------------------------------------------------------------------------- 1 | ../default-init/00-assert.yaml -------------------------------------------------------------------------------- /test/cleanup-image/00-assert.yaml: -------------------------------------------------------------------------------- 1 | ../default-init/00-assert.yaml -------------------------------------------------------------------------------- /test/cleanup-image/02-assert.yaml: -------------------------------------------------------------------------------- 1 | ../default-init/00-assert.yaml -------------------------------------------------------------------------------- /test/cleanup-toggleoverrides/02-cleanup.yaml: -------------------------------------------------------------------------------- 1 | ../cleanup.yaml -------------------------------------------------------------------------------- /test/cleanup-args/00-install.yaml: -------------------------------------------------------------------------------- 1 | ../default-init/00-install.yaml -------------------------------------------------------------------------------- /test/cleanup-args/01-assert.yaml: -------------------------------------------------------------------------------- 1 | ../reconcile-args/01-assert.yaml -------------------------------------------------------------------------------- /test/cleanup-args/01-new-args.yaml: -------------------------------------------------------------------------------- 1 | ../reconcile-args/01-new-args.yaml -------------------------------------------------------------------------------- /test/cleanup-certs/00-install.yaml: -------------------------------------------------------------------------------- 1 | ../default-init/00-install.yaml -------------------------------------------------------------------------------- /test/cleanup-certs/01-assert.yaml: -------------------------------------------------------------------------------- 1 | ../reconcile-certs/01-assert.yaml -------------------------------------------------------------------------------- /test/cleanup-env-vars/00-assert.yaml: -------------------------------------------------------------------------------- 1 | ../default-init/00-assert.yaml -------------------------------------------------------------------------------- /test/cleanup-env-vars/00-install.yaml: -------------------------------------------------------------------------------- 1 | ../default-init/00-install.yaml -------------------------------------------------------------------------------- /test/cleanup-env-vars/02-assert.yaml: -------------------------------------------------------------------------------- 1 | ../default-init/00-assert.yaml -------------------------------------------------------------------------------- /test/cleanup-image/00-install.yaml: -------------------------------------------------------------------------------- 1 | ../default-init/00-install.yaml -------------------------------------------------------------------------------- /test/cleanup-image/01-assert.yaml: -------------------------------------------------------------------------------- 1 | ../reconcile-image/01-assert.yaml -------------------------------------------------------------------------------- /test/cleanup-init-args/00-assert.yaml: -------------------------------------------------------------------------------- 1 | ../default-init/00-assert.yaml -------------------------------------------------------------------------------- /test/cleanup-init-args/02-assert.yaml: -------------------------------------------------------------------------------- 1 | ../default-init/00-assert.yaml -------------------------------------------------------------------------------- /test/cleanup-init-image/00-assert.yaml: -------------------------------------------------------------------------------- 1 | ../default-init/00-assert.yaml -------------------------------------------------------------------------------- /test/cleanup-init-image/02-assert.yaml: -------------------------------------------------------------------------------- 1 | ../default-init/00-assert.yaml -------------------------------------------------------------------------------- /test/cleanup-resources/00-assert.yaml: -------------------------------------------------------------------------------- 1 | ../default-init/00-assert.yaml -------------------------------------------------------------------------------- /test/cleanup-resources/02-assert.yaml: -------------------------------------------------------------------------------- 1 | ../default-init/00-assert.yaml -------------------------------------------------------------------------------- /test/cleanup-services/00-assert.yaml: -------------------------------------------------------------------------------- 1 | ../default-init/00-assert.yaml -------------------------------------------------------------------------------- /test/cleanup-services/00-install.yaml: -------------------------------------------------------------------------------- 1 | ../default-init/00-install.yaml -------------------------------------------------------------------------------- /test/cleanup-services/02-assert.yaml: -------------------------------------------------------------------------------- 1 | ../default-init/00-assert.yaml -------------------------------------------------------------------------------- /test/reconcile-args/00-assert.yaml: -------------------------------------------------------------------------------- 1 | ../default-init/00-assert.yaml -------------------------------------------------------------------------------- /test/reconcile-args/00-install.yaml: -------------------------------------------------------------------------------- 1 | ../default-init/00-install.yaml -------------------------------------------------------------------------------- /test/reconcile-certs/00-assert.yaml: -------------------------------------------------------------------------------- 1 | ../default-init/00-assert.yaml -------------------------------------------------------------------------------- /test/reconcile-certs/00-install.yaml: -------------------------------------------------------------------------------- 1 | ../default-init/00-install.yaml -------------------------------------------------------------------------------- /test/reconcile-env-vars/00-assert.yaml: -------------------------------------------------------------------------------- 1 | ../default-init/00-assert.yaml -------------------------------------------------------------------------------- /test/reconcile-image/00-assert.yaml: -------------------------------------------------------------------------------- 1 | ../default-init/00-assert.yaml -------------------------------------------------------------------------------- /test/reconcile-image/00-install.yaml: -------------------------------------------------------------------------------- 1 | ../default-init/00-install.yaml -------------------------------------------------------------------------------- /test/reconcile-services/00-assert.yaml: -------------------------------------------------------------------------------- 1 | ../default-init/00-assert.yaml -------------------------------------------------------------------------------- /config/prometheus/kustomization.yaml: -------------------------------------------------------------------------------- 1 | resources: 2 | - monitor.yaml 3 | -------------------------------------------------------------------------------- /test/cleanup-certs/01-new-certs.yaml: -------------------------------------------------------------------------------- 1 | ../reconcile-certs/01-new-certs.yaml -------------------------------------------------------------------------------- /test/cleanup-env-vars/01-assert.yaml: -------------------------------------------------------------------------------- 1 | ../reconcile-env-vars/01-assert.yaml -------------------------------------------------------------------------------- /test/cleanup-image/01-new-image.yaml: -------------------------------------------------------------------------------- 1 | ../reconcile-image/01-new-image.yaml -------------------------------------------------------------------------------- /test/cleanup-init-args/00-install.yaml: -------------------------------------------------------------------------------- 1 | ../default-init/00-install.yaml -------------------------------------------------------------------------------- /test/cleanup-init-image/00-install.yaml: -------------------------------------------------------------------------------- 1 | ../default-init/00-install.yaml -------------------------------------------------------------------------------- /test/cleanup-intfmapping/00-assert.yaml: -------------------------------------------------------------------------------- 1 | ../default-init/00-assert.yaml -------------------------------------------------------------------------------- /test/cleanup-intfmapping/00-install.yaml: -------------------------------------------------------------------------------- 1 | ../default-init/00-install.yaml -------------------------------------------------------------------------------- /test/cleanup-intfmapping/02-assert.yaml: -------------------------------------------------------------------------------- 1 | ../default-init/00-assert.yaml -------------------------------------------------------------------------------- /test/cleanup-resources/00-install.yaml: -------------------------------------------------------------------------------- 1 | ../default-init/00-install.yaml -------------------------------------------------------------------------------- /test/cleanup-services/01-assert.yaml: -------------------------------------------------------------------------------- 1 | ../reconcile-services/01-assert.yaml -------------------------------------------------------------------------------- /test/cleanup-toggleoverrides/00-assert.yaml: -------------------------------------------------------------------------------- 1 | ../default-init/00-assert.yaml -------------------------------------------------------------------------------- /test/cleanup-toggleoverrides/02-assert.yaml: -------------------------------------------------------------------------------- 1 | ../default-init/00-assert.yaml -------------------------------------------------------------------------------- /test/cleanup-waitforagents/00-assert.yaml: -------------------------------------------------------------------------------- 1 | ../default-init/00-assert.yaml -------------------------------------------------------------------------------- /test/cleanup-waitforagents/00-install.yaml: -------------------------------------------------------------------------------- 1 | ../default-init/00-install.yaml -------------------------------------------------------------------------------- /test/cleanup-waitforagents/02-assert.yaml: -------------------------------------------------------------------------------- 1 | ../default-init/00-assert.yaml -------------------------------------------------------------------------------- /test/reconcile-env-vars/00-install.yaml: -------------------------------------------------------------------------------- 1 | ../default-init/00-install.yaml -------------------------------------------------------------------------------- /test/reconcile-init-args/00-assert.yaml: -------------------------------------------------------------------------------- 1 | ../default-init/00-assert.yaml -------------------------------------------------------------------------------- /test/reconcile-init-args/00-install.yaml: -------------------------------------------------------------------------------- 1 | ../default-init/00-install.yaml -------------------------------------------------------------------------------- /test/reconcile-init-image/00-assert.yaml: -------------------------------------------------------------------------------- 1 | ../default-init/00-assert.yaml -------------------------------------------------------------------------------- /test/reconcile-init-image/00-install.yaml: -------------------------------------------------------------------------------- 1 | ../default-init/00-install.yaml -------------------------------------------------------------------------------- /test/reconcile-intfmapping/00-assert.yaml: -------------------------------------------------------------------------------- 1 | ../default-init/00-assert.yaml -------------------------------------------------------------------------------- /test/reconcile-intfmapping/00-install.yaml: -------------------------------------------------------------------------------- 1 | ../default-init/00-install.yaml -------------------------------------------------------------------------------- /test/reconcile-resources/00-assert.yaml: -------------------------------------------------------------------------------- 1 | ../default-init/00-assert.yaml -------------------------------------------------------------------------------- /test/reconcile-resources/00-install.yaml: -------------------------------------------------------------------------------- 1 | ../default-init/00-install.yaml -------------------------------------------------------------------------------- /test/reconcile-services/00-install.yaml: -------------------------------------------------------------------------------- 1 | ../default-init/00-install.yaml -------------------------------------------------------------------------------- /test/reconcile-waitforagents/00-assert.yaml: -------------------------------------------------------------------------------- 1 | ../default-init/00-assert.yaml -------------------------------------------------------------------------------- /test/cleanup-env-vars/01-new-vars.yaml: -------------------------------------------------------------------------------- 1 | ../reconcile-env-vars/01-new-vars.yaml -------------------------------------------------------------------------------- /test/cleanup-init-args/01-assert.yaml: -------------------------------------------------------------------------------- 1 | ../reconcile-init-args/01-assert.yaml -------------------------------------------------------------------------------- /test/cleanup-init-args/01-new-args.yaml: -------------------------------------------------------------------------------- 1 | ../reconcile-init-args/01-new-args.yaml -------------------------------------------------------------------------------- /test/cleanup-init-image/01-assert.yaml: -------------------------------------------------------------------------------- 1 | ../reconcile-init-image/01-assert.yaml -------------------------------------------------------------------------------- /test/cleanup-intfmapping/01-assert.yaml: -------------------------------------------------------------------------------- 1 | ../reconcile-intfmapping/01-assert.yaml -------------------------------------------------------------------------------- /test/cleanup-resources/01-assert.yaml: -------------------------------------------------------------------------------- 1 | ../reconcile-resources/01-assert.yaml -------------------------------------------------------------------------------- /test/cleanup-toggleoverrides/00-install.yaml: -------------------------------------------------------------------------------- 1 | ../default-init/00-install.yaml -------------------------------------------------------------------------------- /test/reconcile-toggleoverrides/00-assert.yaml: -------------------------------------------------------------------------------- 1 | ../default-init/00-assert.yaml -------------------------------------------------------------------------------- /test/reconcile-toggleoverrides/00-install.yaml: -------------------------------------------------------------------------------- 1 | ../default-init/00-install.yaml -------------------------------------------------------------------------------- /test/reconcile-waitforagents/00-install.yaml: -------------------------------------------------------------------------------- 1 | ../default-init/00-install.yaml -------------------------------------------------------------------------------- /test/cleanup-waitforagents/01-assert.yaml: -------------------------------------------------------------------------------- 1 | ../reconcile-waitforagents/01-assert.yaml -------------------------------------------------------------------------------- /test/cleanup-resources/01-new-resources.yaml: -------------------------------------------------------------------------------- 1 | ../reconcile-resources/01-new-resources.yaml -------------------------------------------------------------------------------- /test/cleanup-services/01-new-services.yaml: -------------------------------------------------------------------------------- 1 | ../reconcile-services/01-new-services.yaml -------------------------------------------------------------------------------- /test/cleanup-toggleoverrides/01-assert.yaml: -------------------------------------------------------------------------------- 1 | ../reconcile-toggleoverrides/01-assert.yaml -------------------------------------------------------------------------------- /test/cleanup-init-image/01-new-init-image.yaml: -------------------------------------------------------------------------------- 1 | ../reconcile-init-image/01-new-init-image.yaml -------------------------------------------------------------------------------- /test/cleanup-intfmapping/01-new-intfmapping.yaml: -------------------------------------------------------------------------------- 1 | ../reconcile-intfmapping/01-new-intfmapping.yaml -------------------------------------------------------------------------------- /test/cleanup-waitforagents/01-new-waitforagents.yaml: -------------------------------------------------------------------------------- 1 | ../reconcile-waitforagents/01-new-waitforagents.yaml -------------------------------------------------------------------------------- /test/cleanup-toggleoverrides/01-new-toggleoverrides.yaml: -------------------------------------------------------------------------------- 1 | ../reconcile-toggleoverrides/01-new-toggleoverrides.yaml -------------------------------------------------------------------------------- /test/default-init/00-install.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: ceoslab.arista.com/v1alpha1 2 | kind: CEosLabDevice 3 | metadata: 4 | name: ceoslab-device 5 | -------------------------------------------------------------------------------- /config/rbac/service_account.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: v1 2 | kind: ServiceAccount 3 | metadata: 4 | name: controller-manager 5 | namespace: system 6 | -------------------------------------------------------------------------------- /.dockerignore: -------------------------------------------------------------------------------- 1 | # More info: https://docs.docker.com/engine/reference/builder/#dockerignore-file 2 | # Ignore build and test binaries. 3 | bin/ 4 | testbin/ 5 | -------------------------------------------------------------------------------- /test/reconcile-image/01-new-image.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: ceoslab.arista.com/v1alpha1 2 | kind: CEosLabDevice 3 | metadata: 4 | name: ceoslab-device 5 | spec: 6 | image: ceos-2:latest 7 | -------------------------------------------------------------------------------- /config/scorecard/bases/config.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: scorecard.operatorframework.io/v1alpha3 2 | kind: Configuration 3 | metadata: 4 | name: config 5 | stages: 6 | - parallel: true 7 | tests: [] 8 | -------------------------------------------------------------------------------- /test/reconcile-env-vars/01-new-vars.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: ceoslab.arista.com/v1alpha1 2 | kind: CEosLabDevice 3 | metadata: 4 | name: ceoslab-device 5 | spec: 6 | envvars: 7 | FOO: 8 | bar 9 | -------------------------------------------------------------------------------- /test/cleanup.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: kuttl.dev/v1beta1 2 | kind: TestStep 3 | commands: 4 | - command: kubectl patch --type=json --patch-file=../patch.json ceoslabdevices ceoslab-device 5 | namespaced: true 6 | -------------------------------------------------------------------------------- /config/samples/ceoslab_v1alpha1_ceoslabdevice.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: ceoslab.arista.com/v1alpha1 2 | kind: CEosLabDevice 3 | metadata: 4 | name: ceoslabdevice-sample 5 | spec: 6 | # TODO(user): Add fields here 7 | -------------------------------------------------------------------------------- /config/samples/kustomization.yaml: -------------------------------------------------------------------------------- 1 | ## Append samples you want in your CSV to this file as resources ## 2 | resources: 3 | - ceoslab_v1alpha1_ceoslabdevice.yaml 4 | #+kubebuilder:scaffold:manifestskustomizesamples 5 | -------------------------------------------------------------------------------- /test/reconcile-args/01-new-args.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: ceoslab.arista.com/v1alpha1 2 | kind: CEosLabDevice 3 | metadata: 4 | name: ceoslab-device 5 | spec: 6 | args: 7 | - systemd.setenv=PASSED_AS_ARGUMENT=foo 8 | -------------------------------------------------------------------------------- /test/reconcile-init-image/01-new-init-image.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: ceoslab.arista.com/v1alpha1 2 | kind: CEosLabDevice 3 | metadata: 4 | name: ceoslab-device 5 | spec: 6 | initcontainerimage: networkop/init-wait-2:latest 7 | -------------------------------------------------------------------------------- /test/reconcile-resources/01-new-resources.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: ceoslab.arista.com/v1alpha1 2 | kind: CEosLabDevice 3 | metadata: 4 | name: ceoslab-device 5 | spec: 6 | resourcerequirements: 7 | cpu: 8 | "1m" 9 | -------------------------------------------------------------------------------- /test/reconcile-intfmapping/01-new-intfmapping.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: ceoslab.arista.com/v1alpha1 2 | kind: CEosLabDevice 3 | metadata: 4 | name: ceoslab-device 5 | spec: 6 | intfmapping: 7 | eth1: 8 | "Ethernet1/1" 9 | -------------------------------------------------------------------------------- /test/reconcile-waitforagents/01-new-waitforagents.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: ceoslab.arista.com/v1alpha1 2 | kind: CEosLabDevice 3 | metadata: 4 | name: ceoslab-device 5 | spec: 6 | waitforagents: 7 | - ConfigAgent 8 | - Sysdb 9 | -------------------------------------------------------------------------------- /config/rbac/auth_proxy_client_clusterrole.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: rbac.authorization.k8s.io/v1 2 | kind: ClusterRole 3 | metadata: 4 | name: metrics-reader 5 | rules: 6 | - nonResourceURLs: 7 | - "/metrics" 8 | verbs: 9 | - get 10 | -------------------------------------------------------------------------------- /test/reconcile-toggleoverrides/01-new-toggleoverrides.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: ceoslab.arista.com/v1alpha1 2 | kind: CEosLabDevice 3 | metadata: 4 | name: ceoslab-device 5 | spec: 6 | toggleoverrides: 7 | TestFeatureToggle: 8 | true 9 | -------------------------------------------------------------------------------- /config/scorecard/patches/basic.config.yaml: -------------------------------------------------------------------------------- 1 | - op: add 2 | path: /stages/0/tests/- 3 | value: 4 | entrypoint: 5 | - scorecard-test 6 | - basic-check-spec 7 | image: quay.io/operator-framework/scorecard-test:v1.21.0 8 | labels: 9 | suite: basic 10 | test: basic-check-spec-test 11 | -------------------------------------------------------------------------------- /test/reconcile-init-args/01-new-args.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: ceoslab.arista.com/v1alpha1 2 | kind: CEosLabDevice 3 | metadata: 4 | name: ceoslab-device 5 | spec: 6 | # We can't test numinterfaces unfortunately-- the init container will wait indefinitely for the 7 | # new interface to connect with its (nonexistent) peers. 8 | sleep: 1 9 | -------------------------------------------------------------------------------- /config/manager/controller_manager_config.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: controller-runtime.sigs.k8s.io/v1alpha1 2 | kind: ControllerManagerConfig 3 | health: 4 | healthProbeBindAddress: :8081 5 | metrics: 6 | bindAddress: 127.0.0.1:8080 7 | webhook: 8 | port: 9443 9 | leaderElection: 10 | leaderElect: true 11 | resourceName: a8645112.arista.com 12 | -------------------------------------------------------------------------------- /config/rbac/role_binding.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: rbac.authorization.k8s.io/v1 2 | kind: ClusterRoleBinding 3 | metadata: 4 | name: manager-rolebinding 5 | roleRef: 6 | apiGroup: rbac.authorization.k8s.io 7 | kind: ClusterRole 8 | name: manager-role 9 | subjects: 10 | - kind: ServiceAccount 11 | name: controller-manager 12 | namespace: system 13 | -------------------------------------------------------------------------------- /config/rbac/auth_proxy_role_binding.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: rbac.authorization.k8s.io/v1 2 | kind: ClusterRoleBinding 3 | metadata: 4 | name: proxy-rolebinding 5 | roleRef: 6 | apiGroup: rbac.authorization.k8s.io 7 | kind: ClusterRole 8 | name: proxy-role 9 | subjects: 10 | - kind: ServiceAccount 11 | name: controller-manager 12 | namespace: system 13 | -------------------------------------------------------------------------------- /config/crd/patches/cainjection_in_ceoslabdevices.yaml: -------------------------------------------------------------------------------- 1 | # The following patch adds a directive for certmanager to inject CA into the CRD 2 | apiVersion: apiextensions.k8s.io/v1 3 | kind: CustomResourceDefinition 4 | metadata: 5 | annotations: 6 | cert-manager.io/inject-ca-from: $(CERTIFICATE_NAMESPACE)/$(CERTIFICATE_NAME) 7 | name: ceoslabdevices.ceoslab.arista.com 8 | -------------------------------------------------------------------------------- /config/rbac/leader_election_role_binding.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: rbac.authorization.k8s.io/v1 2 | kind: RoleBinding 3 | metadata: 4 | name: leader-election-rolebinding 5 | roleRef: 6 | apiGroup: rbac.authorization.k8s.io 7 | kind: Role 8 | name: leader-election-role 9 | subjects: 10 | - kind: ServiceAccount 11 | name: controller-manager 12 | namespace: system 13 | -------------------------------------------------------------------------------- /config/rbac/auth_proxy_role.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: rbac.authorization.k8s.io/v1 2 | kind: ClusterRole 3 | metadata: 4 | name: proxy-role 5 | rules: 6 | - apiGroups: 7 | - authentication.k8s.io 8 | resources: 9 | - tokenreviews 10 | verbs: 11 | - create 12 | - apiGroups: 13 | - authorization.k8s.io 14 | resources: 15 | - subjectaccessreviews 16 | verbs: 17 | - create 18 | -------------------------------------------------------------------------------- /config/rbac/auth_proxy_service.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: v1 2 | kind: Service 3 | metadata: 4 | labels: 5 | control-plane: controller-manager 6 | name: controller-manager-metrics-service 7 | namespace: system 8 | spec: 9 | ports: 10 | - name: https 11 | port: 8443 12 | protocol: TCP 13 | targetPort: https 14 | selector: 15 | control-plane: controller-manager 16 | -------------------------------------------------------------------------------- /test/reconcile-services/01-new-services.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: ceoslab.arista.com/v1alpha1 2 | kind: CEosLabDevice 3 | metadata: 4 | name: ceoslab-device 5 | spec: 6 | services: 7 | foo: 8 | tcpports: 9 | - in: 22 10 | out: 2022 11 | - in: 25565 12 | gnmi: 13 | tcpports: 14 | - in: 6030 15 | gnoi: 16 | tcpports: 17 | - in: 6030 18 | -------------------------------------------------------------------------------- /config/manager/kustomization.yaml: -------------------------------------------------------------------------------- 1 | resources: 2 | - manager.yaml 3 | 4 | generatorOptions: 5 | disableNameSuffixHash: true 6 | 7 | configMapGenerator: 8 | - files: 9 | - controller_manager_config.yaml 10 | name: manager-config 11 | apiVersion: kustomize.config.k8s.io/v1beta1 12 | kind: Kustomization 13 | images: 14 | - name: controller 15 | newName: ghcr.io/aristanetworks/arista-ceoslab-operator 16 | newTag: v2.1.2 17 | -------------------------------------------------------------------------------- /test/reconcile-certs/01-new-certs.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: ceoslab.arista.com/v1alpha1 2 | kind: CEosLabDevice 3 | metadata: 4 | name: ceoslab-device 5 | spec: 6 | certconfig: 7 | selfsignedcerts: 8 | - certname: test-cert 9 | keyname: test-key 10 | keysize: 4096 11 | commonname: ceoslab-device 12 | - certname: test-cert-2 13 | keyname: test-key-2 14 | keysize: 4096 15 | commonname: ceoslab-device 16 | -------------------------------------------------------------------------------- /test/reconcile-image/01-assert.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: v1 2 | kind: Pod 3 | metadata: 4 | labels: 5 | app: ceoslab-device 6 | name: ceoslab-device 7 | spec: 8 | containers: 9 | - image: ceos-2:latest 10 | name: ceos 11 | status: 12 | containerStatuses: 13 | - name: ceos 14 | ready: true 15 | --- 16 | apiVersion: ceoslab.arista.com/v1alpha1 17 | kind: CEosLabDevice 18 | metadata: 19 | name: ceoslab-device 20 | status: 21 | status: success 22 | -------------------------------------------------------------------------------- /config/rbac/ceoslabdevice_viewer_role.yaml: -------------------------------------------------------------------------------- 1 | # permissions for end users to view ceoslabdevices. 2 | apiVersion: rbac.authorization.k8s.io/v1 3 | kind: ClusterRole 4 | metadata: 5 | name: ceoslabdevice-viewer-role 6 | rules: 7 | - apiGroups: 8 | - ceoslab.arista.com 9 | resources: 10 | - ceoslabdevices 11 | verbs: 12 | - get 13 | - list 14 | - watch 15 | - apiGroups: 16 | - ceoslab.arista.com 17 | resources: 18 | - ceoslabdevices/status 19 | verbs: 20 | - get 21 | -------------------------------------------------------------------------------- /config/scorecard/kustomization.yaml: -------------------------------------------------------------------------------- 1 | resources: 2 | - bases/config.yaml 3 | patchesJson6902: 4 | - path: patches/basic.config.yaml 5 | target: 6 | group: scorecard.operatorframework.io 7 | version: v1alpha3 8 | kind: Configuration 9 | name: config 10 | - path: patches/olm.config.yaml 11 | target: 12 | group: scorecard.operatorframework.io 13 | version: v1alpha3 14 | kind: Configuration 15 | name: config 16 | #+kubebuilder:scaffold:patchesJson6902 17 | -------------------------------------------------------------------------------- /test/reconcile-init-image/01-assert.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: v1 2 | kind: Pod 3 | metadata: 4 | labels: 5 | app: ceoslab-device 6 | name: ceoslab-device 7 | spec: 8 | initContainers: 9 | - image: networkop/init-wait-2:latest 10 | name: init-ceoslab-device 11 | status: 12 | containerStatuses: 13 | - name: ceos 14 | ready: true 15 | --- 16 | apiVersion: ceoslab.arista.com/v1alpha1 17 | kind: CEosLabDevice 18 | metadata: 19 | name: ceoslab-device 20 | status: 21 | status: success 22 | -------------------------------------------------------------------------------- /test/reconcile-resources/01-assert.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: v1 2 | kind: Pod 3 | metadata: 4 | labels: 5 | app: ceoslab-device 6 | name: ceoslab-device 7 | spec: 8 | containers: 9 | - resources: 10 | requests: 11 | cpu: "1m" 12 | name: ceos 13 | status: 14 | containerStatuses: 15 | - name: ceos 16 | ready: true 17 | --- 18 | apiVersion: ceoslab.arista.com/v1alpha1 19 | kind: CEosLabDevice 20 | metadata: 21 | name: ceoslab-device 22 | status: 23 | status: success 24 | -------------------------------------------------------------------------------- /config/crd/patches/webhook_in_ceoslabdevices.yaml: -------------------------------------------------------------------------------- 1 | # The following patch enables a conversion webhook for the CRD 2 | apiVersion: apiextensions.k8s.io/v1 3 | kind: CustomResourceDefinition 4 | metadata: 5 | name: ceoslabdevices.ceoslab.arista.com 6 | spec: 7 | conversion: 8 | strategy: Webhook 9 | webhook: 10 | clientConfig: 11 | service: 12 | namespace: system 13 | name: webhook-service 14 | path: /convert 15 | conversionReviewVersions: 16 | - v1 17 | -------------------------------------------------------------------------------- /kuttl-test.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: kuttl.dev/v1beta1 2 | kind: TestSuite 3 | startKIND: true 4 | kindContext: arista-ceoslab-operator-test 5 | testDirs: 6 | - test 7 | manifestDirs: 8 | - config/kustomized 9 | kindContainers: 10 | - ghcr.io/aristanetworks/arista-ceoslab-operator:v2.1.2 11 | - ceos:latest 12 | - ceos-2:latest 13 | - networkop/init-wait:latest 14 | - networkop/init-wait-2:latest 15 | skipDelete: true 16 | skipClusterDelete: true 17 | # The default timeout of 30 seconds is much to short for ceos to boot reliably 18 | timeout: 120 19 | -------------------------------------------------------------------------------- /config/rbac/ceoslabdevice_editor_role.yaml: -------------------------------------------------------------------------------- 1 | # permissions for end users to edit ceoslabdevices. 2 | apiVersion: rbac.authorization.k8s.io/v1 3 | kind: ClusterRole 4 | metadata: 5 | name: ceoslabdevice-editor-role 6 | rules: 7 | - apiGroups: 8 | - ceoslab.arista.com 9 | resources: 10 | - ceoslabdevices 11 | verbs: 12 | - create 13 | - delete 14 | - get 15 | - list 16 | - patch 17 | - update 18 | - watch 19 | - apiGroups: 20 | - ceoslab.arista.com 21 | resources: 22 | - ceoslabdevices/status 23 | verbs: 24 | - get 25 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | 2 | # Binaries for programs and plugins 3 | *.exe 4 | *.exe~ 5 | *.dll 6 | *.so 7 | *.dylib 8 | bin 9 | testbin/* 10 | 11 | # Test binary, build with `go test -c` 12 | *.test 13 | 14 | # Output of the go coverage tool, specifically when used with LiteIDE 15 | *.out 16 | 17 | # Kubernetes Generated files - skip generated files, except for vendored files 18 | 19 | !vendor/**/zz_generated.* 20 | 21 | # editor and IDE paraphernalia 22 | .idea 23 | *.swp 24 | *.swo 25 | *~ 26 | 27 | # kuttl generated files 28 | kubeconfig 29 | kind-logs-* 30 | -------------------------------------------------------------------------------- /PROJECT: -------------------------------------------------------------------------------- 1 | domain: arista.com 2 | layout: 3 | - go.kubebuilder.io/v3 4 | plugins: 5 | manifests.sdk.operatorframework.io/v2: {} 6 | scorecard.sdk.operatorframework.io/v2: {} 7 | projectName: arista-ceoslab-operator 8 | repo: github.com/aristanetworks/arista-ceoslab-operator 9 | resources: 10 | - api: 11 | crdVersion: v1 12 | namespaced: true 13 | controller: true 14 | domain: arista.com 15 | group: ceoslab 16 | kind: CEosLabDevice 17 | path: github.com/aristanetworks/arista-ceoslab-operator/api/v1alpha1 18 | version: v1alpha1 19 | version: "3" 20 | -------------------------------------------------------------------------------- /config/default/manager_config_patch.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: apps/v1 2 | kind: Deployment 3 | metadata: 4 | name: controller-manager 5 | namespace: system 6 | spec: 7 | template: 8 | spec: 9 | containers: 10 | - name: manager 11 | args: 12 | - "--config=controller_manager_config.yaml" 13 | volumeMounts: 14 | - name: manager-config 15 | mountPath: /controller_manager_config.yaml 16 | subPath: controller_manager_config.yaml 17 | volumes: 18 | - name: manager-config 19 | configMap: 20 | name: manager-config 21 | -------------------------------------------------------------------------------- /config/prometheus/monitor.yaml: -------------------------------------------------------------------------------- 1 | 2 | # Prometheus Monitor Service (Metrics) 3 | apiVersion: monitoring.coreos.com/v1 4 | kind: ServiceMonitor 5 | metadata: 6 | labels: 7 | control-plane: controller-manager 8 | name: controller-manager-metrics-monitor 9 | namespace: system 10 | spec: 11 | endpoints: 12 | - path: /metrics 13 | port: https 14 | scheme: https 15 | bearerTokenFile: /var/run/secrets/kubernetes.io/serviceaccount/token 16 | tlsConfig: 17 | insecureSkipVerify: true 18 | selector: 19 | matchLabels: 20 | control-plane: controller-manager 21 | -------------------------------------------------------------------------------- /test/reconcile-init-args/01-assert.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: v1 2 | kind: Pod 3 | metadata: 4 | labels: 5 | app: ceoslab-device 6 | name: ceoslab-device 7 | spec: 8 | initContainers: 9 | - args: 10 | - "1" 11 | - "1" 12 | image: networkop/init-wait:latest 13 | imagePullPolicy: IfNotPresent 14 | name: init-ceoslab-device 15 | resources: {} 16 | status: 17 | containerStatuses: 18 | - name: ceos 19 | ready: true 20 | --- 21 | apiVersion: ceoslab.arista.com/v1alpha1 22 | kind: CEosLabDevice 23 | metadata: 24 | name: ceoslab-device 25 | status: 26 | status: success 27 | -------------------------------------------------------------------------------- /test/reconcile-waitforagents/01-assert.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: v1 2 | kind: Pod 3 | metadata: 4 | labels: 5 | app: ceoslab-device 6 | name: ceoslab-device 7 | spec: 8 | containers: 9 | - name: ceos 10 | startupProbe: 11 | exec: 12 | command: 13 | - wfw 14 | - -t 15 | - "5" 16 | - ConfigAgent 17 | - Sysdb 18 | status: 19 | containerStatuses: 20 | - name: ceos 21 | ready: true 22 | --- 23 | apiVersion: ceoslab.arista.com/v1alpha1 24 | kind: CEosLabDevice 25 | metadata: 26 | name: ceoslab-device 27 | status: 28 | status: success 29 | -------------------------------------------------------------------------------- /config/crd/kustomizeconfig.yaml: -------------------------------------------------------------------------------- 1 | # This file is for teaching kustomize how to substitute name and namespace reference in CRD 2 | nameReference: 3 | - kind: Service 4 | version: v1 5 | fieldSpecs: 6 | - kind: CustomResourceDefinition 7 | version: v1 8 | group: apiextensions.k8s.io 9 | path: spec/conversion/webhook/clientConfig/service/name 10 | 11 | namespace: 12 | - kind: CustomResourceDefinition 13 | version: v1 14 | group: apiextensions.k8s.io 15 | path: spec/conversion/webhook/clientConfig/service/namespace 16 | create: false 17 | 18 | varReference: 19 | - path: metadata/annotations 20 | -------------------------------------------------------------------------------- /hack/boilerplate.go.txt: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2022. 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ -------------------------------------------------------------------------------- /config/rbac/leader_election_role.yaml: -------------------------------------------------------------------------------- 1 | # permissions to do leader election. 2 | apiVersion: rbac.authorization.k8s.io/v1 3 | kind: Role 4 | metadata: 5 | name: leader-election-role 6 | rules: 7 | - apiGroups: 8 | - "" 9 | resources: 10 | - configmaps 11 | verbs: 12 | - get 13 | - list 14 | - watch 15 | - create 16 | - update 17 | - patch 18 | - delete 19 | - apiGroups: 20 | - coordination.k8s.io 21 | resources: 22 | - leases 23 | verbs: 24 | - get 25 | - list 26 | - watch 27 | - create 28 | - update 29 | - patch 30 | - delete 31 | - apiGroups: 32 | - "" 33 | resources: 34 | - events 35 | verbs: 36 | - create 37 | - patch 38 | -------------------------------------------------------------------------------- /test/patch.json: -------------------------------------------------------------------------------- 1 | [ 2 | {"op": "replace", "path": "/spec/envvars", "value": {}}, 3 | {"op": "replace", "path": "/spec/image", "value": ""}, 4 | {"op": "replace", "path": "/spec/args", "value": []}, 5 | {"op": "replace", "path": "/spec/resourcerequirements", "value": {}}, 6 | {"op": "replace", "path": "/spec/services", "value": {}}, 7 | {"op": "replace", "path": "/spec/initcontainerimage", "value": ""}, 8 | {"op": "replace", "path": "/spec/sleep", "value": 0}, 9 | {"op": "replace", "path": "/spec/certconfig", "value": {}}, 10 | {"op": "replace", "path": "/spec/intfmapping", "value": {}}, 11 | {"op": "replace", "path": "/spec/toggleoverrides", "value": {}}, 12 | {"op": "replace", "path": "/spec/waitforagents", "value": []} 13 | ] 14 | -------------------------------------------------------------------------------- /config/rbac/kustomization.yaml: -------------------------------------------------------------------------------- 1 | resources: 2 | # All RBAC will be applied under this service account in 3 | # the deployment namespace. You may comment out this resource 4 | # if your manager will use a service account that exists at 5 | # runtime. Be sure to update RoleBinding and ClusterRoleBinding 6 | # subjects if changing service account names. 7 | - service_account.yaml 8 | - role.yaml 9 | - role_binding.yaml 10 | - leader_election_role.yaml 11 | - leader_election_role_binding.yaml 12 | # Comment the following 4 lines if you want to disable 13 | # the auth proxy (https://github.com/brancz/kube-rbac-proxy) 14 | # which protects your /metrics endpoint. 15 | - auth_proxy_service.yaml 16 | - auth_proxy_role.yaml 17 | - auth_proxy_role_binding.yaml 18 | - auth_proxy_client_clusterrole.yaml 19 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | # Build the manager binary 2 | FROM golang:1.17 as builder 3 | 4 | WORKDIR /workspace 5 | # Copy the Go Modules manifests 6 | COPY go.mod go.mod 7 | COPY go.sum go.sum 8 | # cache deps before building and copying source so that we don't need to re-download as much 9 | # and so that source changes don't invalidate our downloaded layer 10 | RUN go mod download 11 | 12 | # Copy the go source 13 | COPY main.go main.go 14 | COPY api/ api/ 15 | COPY controllers/ controllers/ 16 | 17 | # Build 18 | RUN CGO_ENABLED=0 GOOS=linux GOARCH=amd64 go build -a -o manager main.go 19 | 20 | # Use distroless as minimal base image to package the manager binary 21 | # Refer to https://github.com/GoogleContainerTools/distroless for more details 22 | FROM gcr.io/distroless/static:nonroot 23 | WORKDIR / 24 | COPY --from=builder /workspace/manager . 25 | USER 65532:65532 26 | 27 | ENTRYPOINT ["/manager"] 28 | -------------------------------------------------------------------------------- /config/crd/kustomization.yaml: -------------------------------------------------------------------------------- 1 | # This kustomization.yaml is not intended to be run by itself, 2 | # since it depends on service name and namespace that are out of this kustomize package. 3 | # It should be run by config/default 4 | resources: 5 | - bases/ceoslab.arista.com_ceoslabdevices.yaml 6 | #+kubebuilder:scaffold:crdkustomizeresource 7 | 8 | patchesStrategicMerge: 9 | # [WEBHOOK] To enable webhook, uncomment all the sections with [WEBHOOK] prefix. 10 | # patches here are for enabling the conversion webhook for each CRD 11 | #- patches/webhook_in_ceoslabdevices.yaml 12 | #+kubebuilder:scaffold:crdkustomizewebhookpatch 13 | 14 | # [CERTMANAGER] To enable cert-manager, uncomment all the sections with [CERTMANAGER] prefix. 15 | # patches here are for enabling the CA injection for each CRD 16 | #- patches/cainjection_in_ceoslabdevices.yaml 17 | #+kubebuilder:scaffold:crdkustomizecainjectionpatch 18 | 19 | # the following config is for teaching kustomize how to do kustomization for CRDs. 20 | configurations: 21 | - kustomizeconfig.yaml 22 | -------------------------------------------------------------------------------- /test/reconcile-args/01-assert.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: v1 2 | kind: Pod 3 | metadata: 4 | labels: 5 | app: ceoslab-device 6 | name: ceoslab-device 7 | spec: 8 | containers: 9 | - args: 10 | - systemd.setenv=CEOS=1 11 | - systemd.setenv=EOS_PLATFORM=ceoslab 12 | - systemd.setenv=ETBA=1 13 | - systemd.setenv=INTFTYPE=eth 14 | - systemd.setenv=SKIP_ZEROTOUCH_BARRIER_IN_SYSDBINIT=1 15 | - systemd.setenv=container=docker 16 | - systemd.setenv=PASSED_AS_ARGUMENT=foo 17 | env: 18 | - name: CEOS 19 | value: "1" 20 | - name: EOS_PLATFORM 21 | value: ceoslab 22 | - name: ETBA 23 | value: "1" 24 | - name: INTFTYPE 25 | value: eth 26 | - name: SKIP_ZEROTOUCH_BARRIER_IN_SYSDBINIT 27 | value: "1" 28 | - name: container 29 | value: docker 30 | name: ceos 31 | status: 32 | containerStatuses: 33 | - name: ceos 34 | ready: true 35 | --- 36 | apiVersion: ceoslab.arista.com/v1alpha1 37 | kind: CEosLabDevice 38 | metadata: 39 | name: ceoslab-device 40 | status: 41 | status: success 42 | -------------------------------------------------------------------------------- /test/reconcile-env-vars/01-assert.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: v1 2 | kind: Pod 3 | metadata: 4 | labels: 5 | app: ceoslab-device 6 | name: ceoslab-device 7 | spec: 8 | containers: 9 | - args: 10 | - systemd.setenv=CEOS=1 11 | - systemd.setenv=EOS_PLATFORM=ceoslab 12 | - systemd.setenv=ETBA=1 13 | - systemd.setenv=FOO=bar 14 | - systemd.setenv=INTFTYPE=eth 15 | - systemd.setenv=SKIP_ZEROTOUCH_BARRIER_IN_SYSDBINIT=1 16 | - systemd.setenv=container=docker 17 | env: 18 | - name: CEOS 19 | value: "1" 20 | - name: EOS_PLATFORM 21 | value: ceoslab 22 | - name: ETBA 23 | value: "1" 24 | - name: FOO 25 | value: bar 26 | - name: INTFTYPE 27 | value: eth 28 | - name: SKIP_ZEROTOUCH_BARRIER_IN_SYSDBINIT 29 | value: "1" 30 | - name: container 31 | value: docker 32 | name: ceos 33 | status: 34 | containerStatuses: 35 | - name: ceos 36 | ready: true 37 | --- 38 | apiVersion: ceoslab.arista.com/v1alpha1 39 | kind: CEosLabDevice 40 | metadata: 41 | name: ceoslab-device 42 | status: 43 | status: success 44 | -------------------------------------------------------------------------------- /config/default/manager_auth_proxy_patch.yaml: -------------------------------------------------------------------------------- 1 | # This patch inject a sidecar container which is a HTTP proxy for the 2 | # controller manager, it performs RBAC authorization against the Kubernetes API using SubjectAccessReviews. 3 | apiVersion: apps/v1 4 | kind: Deployment 5 | metadata: 6 | name: controller-manager 7 | namespace: system 8 | spec: 9 | template: 10 | spec: 11 | containers: 12 | - name: kube-rbac-proxy 13 | image: gcr.io/kubebuilder/kube-rbac-proxy:v0.11.0 14 | args: 15 | - "--secure-listen-address=0.0.0.0:8443" 16 | - "--upstream=http://127.0.0.1:8080/" 17 | - "--logtostderr=true" 18 | - "--v=0" 19 | ports: 20 | - containerPort: 8443 21 | protocol: TCP 22 | name: https 23 | resources: 24 | limits: 25 | cpu: 500m 26 | memory: 128Mi 27 | requests: 28 | cpu: 5m 29 | memory: 64Mi 30 | - name: manager 31 | args: 32 | - "--health-probe-bind-address=:8081" 33 | - "--metrics-bind-address=127.0.0.1:8080" 34 | - "--leader-elect" 35 | -------------------------------------------------------------------------------- /test/reconcile-services/01-assert.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: v1 2 | kind: Pod 3 | metadata: 4 | labels: 5 | app: ceoslab-device 6 | name: ceoslab-device 7 | spec: 8 | containers: 9 | - name: ceos 10 | status: 11 | containerStatuses: 12 | - name: ceos 13 | ready: true 14 | --- 15 | apiVersion: ceoslab.arista.com/v1alpha1 16 | kind: CEosLabDevice 17 | metadata: 18 | name: ceoslab-device 19 | status: 20 | status: success 21 | --- 22 | apiVersion: v1 23 | kind: Service 24 | metadata: 25 | labels: 26 | pod: ceoslab-device 27 | name: service-ceoslab-device 28 | ownerReferences: 29 | - apiVersion: ceoslab.arista.com/v1alpha1 30 | blockOwnerDeletion: true 31 | controller: true 32 | kind: CEosLabDevice 33 | name: ceoslab-device 34 | spec: 35 | ports: 36 | - name: foo22 37 | port: 2022 38 | protocol: TCP 39 | targetPort: 22 40 | - name: foo25565 41 | port: 25565 42 | protocol: TCP 43 | targetPort: 25565 44 | - name: gnmi6030 45 | port: 6030 46 | protocol: TCP 47 | targetPort: 6030 48 | # gNOI port ignored because it's the same as gNMI 49 | selector: 50 | app: ceoslab-device 51 | type: LoadBalancer 52 | -------------------------------------------------------------------------------- /test/reconcile-intfmapping/01-assert.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: v1 2 | kind: Pod 3 | metadata: 4 | labels: 5 | app: ceoslab-device 6 | name: ceoslab-device 7 | spec: 8 | containers: 9 | - volumeMounts: 10 | - mountPath: /mnt/flash/EosIntfMapping.json 11 | name: volume-configmap-intfmapping-ceoslab-device 12 | subPath: EosIntfMapping.json 13 | - mountPath: /var/run/secrets/kubernetes.io/serviceaccount 14 | name: ceos 15 | status: 16 | containerStatuses: 17 | - name: ceos 18 | ready: true 19 | --- 20 | apiVersion: ceoslab.arista.com/v1alpha1 21 | kind: CEosLabDevice 22 | metadata: 23 | name: ceoslab-device 24 | spec: 25 | intfmapping: 26 | eth1: Ethernet1/1 27 | status: 28 | configmapconfig: 29 | intfmappingstatus: 30 | eth1: Ethernet1/1 31 | podconfigmapconfig: 32 | intfmappingstatus: 33 | eth1: Ethernet1/1 34 | status: success 35 | --- 36 | apiVersion: v1 37 | kind: ConfigMap 38 | metadata: 39 | name: configmap-intfmapping-ceoslab-device 40 | ownerReferences: 41 | - apiVersion: ceoslab.arista.com/v1alpha1 42 | blockOwnerDeletion: true 43 | controller: true 44 | kind: CEosLabDevice 45 | name: ceoslab-device 46 | -------------------------------------------------------------------------------- /test/reconcile-toggleoverrides/01-assert.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: v1 2 | kind: Pod 3 | metadata: 4 | labels: 5 | app: ceoslab-device 6 | name: ceoslab-device 7 | spec: 8 | containers: 9 | - volumeMounts: 10 | - mountPath: /mnt/flash/toggle_override 11 | name: volume-configmap-toggle-override-ceoslab-device 12 | subPath: toggle_override 13 | - mountPath: /var/run/secrets/kubernetes.io/serviceaccount 14 | name: ceos 15 | status: 16 | containerStatuses: 17 | - name: ceos 18 | ready: true 19 | --- 20 | apiVersion: ceoslab.arista.com/v1alpha1 21 | kind: CEosLabDevice 22 | metadata: 23 | name: ceoslab-device 24 | spec: 25 | toggleoverrides: 26 | TestFeatureToggle: true 27 | status: 28 | configmapconfig: 29 | toggleoverridesstatus: 30 | TestFeatureToggle: true 31 | podconfigmapconfig: 32 | toggleoverridesstatus: 33 | TestFeatureToggle: true 34 | status: success 35 | --- 36 | apiVersion: v1 37 | kind: ConfigMap 38 | metadata: 39 | name: configmap-toggle-override-ceoslab-device 40 | ownerReferences: 41 | - apiVersion: ceoslab.arista.com/v1alpha1 42 | blockOwnerDeletion: true 43 | controller: true 44 | kind: CEosLabDevice 45 | name: ceoslab-device 46 | -------------------------------------------------------------------------------- /config/manifests/kustomization.yaml: -------------------------------------------------------------------------------- 1 | # These resources constitute the fully configured set of manifests 2 | # used to generate the 'manifests/' directory in a bundle. 3 | resources: 4 | - bases/arista-ceoslab-operator.clusterserviceversion.yaml 5 | - ../default 6 | - ../samples 7 | - ../scorecard 8 | 9 | # [WEBHOOK] To enable webhooks, uncomment all the sections with [WEBHOOK] prefix. 10 | # Do NOT uncomment sections with prefix [CERTMANAGER], as OLM does not support cert-manager. 11 | # These patches remove the unnecessary "cert" volume and its manager container volumeMount. 12 | #patchesJson6902: 13 | #- target: 14 | # group: apps 15 | # version: v1 16 | # kind: Deployment 17 | # name: controller-manager 18 | # namespace: system 19 | # patch: |- 20 | # # Remove the manager container's "cert" volumeMount, since OLM will create and mount a set of certs. 21 | # # Update the indices in this path if adding or removing containers/volumeMounts in the manager's Deployment. 22 | # - op: remove 23 | # path: /spec/template/spec/containers/1/volumeMounts/0 24 | # # Remove the "cert" volume, since OLM will create and mount a set of certs. 25 | # # Update the indices in this path if adding or removing volumes in the manager's Deployment. 26 | # - op: remove 27 | # path: /spec/template/spec/volumes/0 28 | -------------------------------------------------------------------------------- /config/rbac/role.yaml: -------------------------------------------------------------------------------- 1 | --- 2 | apiVersion: rbac.authorization.k8s.io/v1 3 | kind: ClusterRole 4 | metadata: 5 | creationTimestamp: null 6 | name: manager-role 7 | rules: 8 | - apiGroups: 9 | - ceoslab.arista.com 10 | resources: 11 | - ceoslabdevices 12 | verbs: 13 | - create 14 | - delete 15 | - get 16 | - list 17 | - patch 18 | - update 19 | - watch 20 | - apiGroups: 21 | - ceoslab.arista.com 22 | resources: 23 | - ceoslabdevices/finalizers 24 | verbs: 25 | - update 26 | - apiGroups: 27 | - ceoslab.arista.com 28 | resources: 29 | - ceoslabdevices/status 30 | verbs: 31 | - get 32 | - patch 33 | - update 34 | - apiGroups: 35 | - "" 36 | resources: 37 | - configmaps 38 | verbs: 39 | - create 40 | - delete 41 | - get 42 | - list 43 | - patch 44 | - update 45 | - watch 46 | - apiGroups: 47 | - "" 48 | resources: 49 | - pods 50 | verbs: 51 | - create 52 | - delete 53 | - get 54 | - list 55 | - patch 56 | - update 57 | - watch 58 | - apiGroups: 59 | - "" 60 | resources: 61 | - secrets 62 | verbs: 63 | - create 64 | - delete 65 | - get 66 | - list 67 | - patch 68 | - update 69 | - watch 70 | - apiGroups: 71 | - "" 72 | resources: 73 | - services 74 | verbs: 75 | - create 76 | - delete 77 | - get 78 | - list 79 | - patch 80 | - update 81 | - watch 82 | -------------------------------------------------------------------------------- /api/v1alpha1/dynamic/fake/client.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2022. 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | 17 | package fake 18 | 19 | import ( 20 | "github.com/aristanetworks/arista-ceoslab-operator/v2/api/v1alpha1" 21 | "github.com/aristanetworks/arista-ceoslab-operator/v2/api/v1alpha1/dynamic" 22 | "k8s.io/apimachinery/pkg/runtime" 23 | fakeclient "k8s.io/client-go/dynamic/fake" 24 | "k8s.io/client-go/rest" 25 | ) 26 | 27 | func NewSimpleClientset(objects ...runtime.Object) (*dynamic.CEosLabDeviceV1Alpha1Client, error) { 28 | ceosClient, err := dynamic.NewForConfig(&rest.Config{}) 29 | if err != nil { 30 | return nil, err 31 | } 32 | scheme := runtime.NewScheme() 33 | v1alpha1.AddToScheme(scheme) 34 | dynamicClient := fakeclient.NewSimpleDynamicClient(scheme, objects...) 35 | ceosClient.SetDeviceClient(dynamicClient.Resource(v1alpha1.GroupVersionResource)) 36 | return ceosClient, nil 37 | } 38 | -------------------------------------------------------------------------------- /config/scorecard/patches/olm.config.yaml: -------------------------------------------------------------------------------- 1 | - op: add 2 | path: /stages/0/tests/- 3 | value: 4 | entrypoint: 5 | - scorecard-test 6 | - olm-bundle-validation 7 | image: quay.io/operator-framework/scorecard-test:v1.21.0 8 | labels: 9 | suite: olm 10 | test: olm-bundle-validation-test 11 | - op: add 12 | path: /stages/0/tests/- 13 | value: 14 | entrypoint: 15 | - scorecard-test 16 | - olm-crds-have-validation 17 | image: quay.io/operator-framework/scorecard-test:v1.21.0 18 | labels: 19 | suite: olm 20 | test: olm-crds-have-validation-test 21 | - op: add 22 | path: /stages/0/tests/- 23 | value: 24 | entrypoint: 25 | - scorecard-test 26 | - olm-crds-have-resources 27 | image: quay.io/operator-framework/scorecard-test:v1.21.0 28 | labels: 29 | suite: olm 30 | test: olm-crds-have-resources-test 31 | - op: add 32 | path: /stages/0/tests/- 33 | value: 34 | entrypoint: 35 | - scorecard-test 36 | - olm-spec-descriptors 37 | image: quay.io/operator-framework/scorecard-test:v1.21.0 38 | labels: 39 | suite: olm 40 | test: olm-spec-descriptors-test 41 | - op: add 42 | path: /stages/0/tests/- 43 | value: 44 | entrypoint: 45 | - scorecard-test 46 | - olm-status-descriptors 47 | image: quay.io/operator-framework/scorecard-test:v1.21.0 48 | labels: 49 | suite: olm 50 | test: olm-status-descriptors-test 51 | -------------------------------------------------------------------------------- /config/manifests/bases/arista-ceoslab-operator.clusterserviceversion.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: operators.coreos.com/v1alpha1 2 | kind: ClusterServiceVersion 3 | metadata: 4 | annotations: 5 | alm-examples: '[]' 6 | capabilities: Basic Install 7 | name: arista-ceoslab-operator.v0.0.0 8 | namespace: placeholder 9 | spec: 10 | apiservicedefinitions: {} 11 | customresourcedefinitions: 12 | owned: 13 | - description: CEosLabDevice is the Schema for the ceoslabdevices API 14 | displayName: CEos Lab Device 15 | kind: CEosLabDevice 16 | name: ceoslabdevices.ceoslab.arista.com 17 | version: v1alpha1 18 | description: K8s operator for managing meshnet-networked cEOS-lab instances 19 | displayName: arista-ceoslab-operator 20 | icon: 21 | - base64data: "" 22 | mediatype: "" 23 | install: 24 | spec: 25 | deployments: null 26 | strategy: "" 27 | installModes: 28 | - supported: false 29 | type: OwnNamespace 30 | - supported: false 31 | type: SingleNamespace 32 | - supported: false 33 | type: MultiNamespace 34 | - supported: true 35 | type: AllNamespaces 36 | keywords: 37 | - KNE 38 | - meshnet 39 | - ceoslab 40 | - Arista 41 | links: 42 | - name: Arista Ceoslab Operator 43 | url: https://arista-ceoslab-operator.domain 44 | maturity: alpha 45 | provider: 46 | name: Arista Networks 47 | url: https://github.com/aristanetworks/arista-ceoslab-operator 48 | version: 0.0.0 49 | -------------------------------------------------------------------------------- /test/default-init/00-assert.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: v1 2 | kind: Pod 3 | metadata: 4 | labels: 5 | app: ceoslab-device 6 | name: ceoslab-device 7 | spec: 8 | containers: 9 | - args: 10 | - systemd.setenv=CEOS=1 11 | - systemd.setenv=EOS_PLATFORM=ceoslab 12 | - systemd.setenv=ETBA=1 13 | - systemd.setenv=INTFTYPE=eth 14 | - systemd.setenv=SKIP_ZEROTOUCH_BARRIER_IN_SYSDBINIT=1 15 | - systemd.setenv=container=docker 16 | command: 17 | - /sbin/init 18 | env: 19 | - name: CEOS 20 | value: "1" 21 | - name: EOS_PLATFORM 22 | value: ceoslab 23 | - name: ETBA 24 | value: "1" 25 | - name: INTFTYPE 26 | value: eth 27 | - name: SKIP_ZEROTOUCH_BARRIER_IN_SYSDBINIT 28 | value: "1" 29 | - name: container 30 | value: docker 31 | image: ceos:latest 32 | imagePullPolicy: IfNotPresent 33 | name: ceos 34 | resources: {} 35 | securityContext: 36 | privileged: true 37 | startupProbe: 38 | exec: 39 | command: 40 | - wfw 41 | - -t 42 | - "5" 43 | failureThreshold: 24 44 | periodSeconds: 5 45 | successThreshold: 1 46 | timeoutSeconds: 5 47 | initContainers: 48 | - args: 49 | - "1" 50 | - "0" 51 | image: networkop/init-wait:latest 52 | imagePullPolicy: IfNotPresent 53 | name: init-ceoslab-device 54 | resources: {} 55 | status: 56 | containerStatuses: 57 | - name: ceos 58 | ready: true 59 | --- 60 | apiVersion: ceoslab.arista.com/v1alpha1 61 | kind: CEosLabDevice 62 | metadata: 63 | name: ceoslab-device 64 | status: 65 | configmapconfig: {} 66 | podconfigmapconfig: {} 67 | status: success 68 | -------------------------------------------------------------------------------- /api/v1alpha1/client/interface.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2022. 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | 17 | package client 18 | 19 | import ( 20 | "context" 21 | 22 | "github.com/aristanetworks/arista-ceoslab-operator/v2/api/v1alpha1" 23 | metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" 24 | "k8s.io/apimachinery/pkg/types" 25 | "k8s.io/apimachinery/pkg/watch" 26 | ) 27 | 28 | type CEosLabDeviceV1Alpha1Interface interface { 29 | CEosLabDevices(namespace string) CEosLabDeviceOpsInterface 30 | } 31 | 32 | type CEosLabDeviceOpsInterface interface { 33 | List(ctx context.Context, opts metav1.ListOptions) (*v1alpha1.CEosLabDeviceList, error) 34 | Get(ctx context.Context, name string, opts metav1.GetOptions) (*v1alpha1.CEosLabDevice, error) 35 | Create(ctx context.Context, device *v1alpha1.CEosLabDevice, opts metav1.CreateOptions) (*v1alpha1.CEosLabDevice, error) 36 | Watch(ctx context.Context, opts metav1.ListOptions) (watch.Interface, error) 37 | Delete(ctx context.Context, name string, opts metav1.DeleteOptions) error 38 | Patch(ctx context.Context, name string, pt types.PatchType, data []byte, opts metav1.PatchOptions, subresources ...string) (result *v1alpha1.CEosLabDevice, err error) 39 | } 40 | -------------------------------------------------------------------------------- /config/manager/manager.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: v1 2 | kind: Namespace 3 | metadata: 4 | labels: 5 | control-plane: controller-manager 6 | name: system 7 | --- 8 | apiVersion: apps/v1 9 | kind: Deployment 10 | metadata: 11 | name: controller-manager 12 | namespace: system 13 | labels: 14 | control-plane: controller-manager 15 | spec: 16 | selector: 17 | matchLabels: 18 | control-plane: controller-manager 19 | replicas: 1 20 | template: 21 | metadata: 22 | annotations: 23 | kubectl.kubernetes.io/default-container: manager 24 | labels: 25 | control-plane: controller-manager 26 | spec: 27 | securityContext: 28 | runAsNonRoot: true 29 | containers: 30 | - command: 31 | - /manager 32 | args: 33 | - --leader-elect 34 | image: controller:latest 35 | name: manager 36 | securityContext: 37 | allowPrivilegeEscalation: false 38 | livenessProbe: 39 | httpGet: 40 | path: /healthz 41 | port: 8081 42 | initialDelaySeconds: 15 43 | periodSeconds: 20 44 | readinessProbe: 45 | httpGet: 46 | path: /readyz 47 | port: 8081 48 | initialDelaySeconds: 5 49 | periodSeconds: 10 50 | # TODO(user): Configure the resources accordingly based on the project requirements. 51 | # More info: https://kubernetes.io/docs/concepts/configuration/manage-resources-containers/ 52 | resources: 53 | limits: 54 | cpu: 500m 55 | memory: 128Mi 56 | requests: 57 | cpu: 10m 58 | memory: 64Mi 59 | serviceAccountName: controller-manager 60 | terminationGracePeriodSeconds: 10 61 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Arista cEOS-lab Operator 2 | The Arista Networks cEOS-lab operator manages containerized EOS instances in k8s clusters managed 3 | by KNE. It is based on operator-sdk. 4 | 5 | ## Usage 6 | Deploy the manifest: 7 | ``` 8 | kubectl apply -f config/kustomized/manifest.yaml 9 | ``` 10 | This should be equivalent to `-k config/bases`. However, unit tests are run against the manifest 11 | so this is preferred. 12 | 13 | ## Development Notes 14 | 15 | ### Testing 16 | The operator uses [kuttl](https://kuttl.dev/) for testing. To run the test suite, make sure images 17 | of the operator, cEOS-lab, and the init container are tagged as named in `kuttl-test.yaml` in the 18 | repository's root. Kuttl can be used either through its binary or `kubectl kuttl`. 19 | 20 | As mentioned above, these tests use the manifest file in `config/kustomized` to stage the test 21 | cluster (kuttl does not support kustomize). If relevant changes are made to the kustomization files 22 | or Makefile, for instance, when changing the version/tag or image name, this manifest should 23 | be updated by `make manifests`. 24 | 25 | ### Versioning 26 | The git tag should match the project version defined in the Makefile, the image tag present in the 27 | manifest, and the image tag in `kuttl-test.yaml`. 28 | 29 | ## How it works 30 | This project aims to follow the Kubernetes [Operator pattern](https://kubernetes.io/docs/concepts/extend-kubernetes/operator/) 31 | 32 | It uses [Controllers](https://kubernetes.io/docs/concepts/architecture/controller/) 33 | which provides a reconcile function responsible for synchronizing resources untile the desired state is reached on the cluster 34 | 35 | ## License 36 | 37 | Copyright 2022 Arista Networks 38 | 39 | Licensed under the Apache License, Version 2.0 (the "License"); 40 | you may not use this file except in compliance with the License. 41 | You may obtain a copy of the License at 42 | 43 | http://www.apache.org/licenses/LICENSE-2.0 44 | 45 | Unless required by applicable law or agreed to in writing, software 46 | distributed under the License is distributed on an "AS IS" BASIS, 47 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 48 | See the License for the specific language governing permissions and 49 | limitations under the License. 50 | -------------------------------------------------------------------------------- /api/v1alpha1/groupversion_info.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2022. 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | 17 | // Package v1alpha1 contains API Schema definitions for the ceoslab v1alpha1 API group 18 | //+kubebuilder:object:generate=true 19 | //+groupName=ceoslab.arista.com 20 | package v1alpha1 21 | 22 | import ( 23 | metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" 24 | "k8s.io/apimachinery/pkg/runtime" 25 | "k8s.io/apimachinery/pkg/runtime/schema" 26 | "k8s.io/client-go/kubernetes/scheme" 27 | ) 28 | 29 | var ( 30 | group = "ceoslab.arista.com" 31 | version = "v1alpha1" 32 | resource = "ceoslabdevices" 33 | kind = "CEosLabDevice" 34 | 35 | // GroupVersionResource is used to register and type these objects 36 | GroupVersionResource = schema.GroupVersionResource{Group: group, Version: version, Resource: resource} 37 | 38 | // Used by the dynamic client to create a new device (Kind) 39 | GroupVersionKind = schema.GroupVersionKind{Group: group, Version: version, Kind: kind} 40 | 41 | // GroupVersion is group version used to register these objects 42 | GroupVersion = schema.GroupVersion{Group: group, Version: version} 43 | 44 | // SchemeBuilder is used to add go types to the GroupVersionKind scheme 45 | SchemeBuilder = runtime.NewSchemeBuilder(addKnownTypes) 46 | 47 | // AddToScheme adds the types in this group-version to the given scheme. 48 | AddToScheme = SchemeBuilder.AddToScheme 49 | ) 50 | 51 | func addKnownTypes(scheme *runtime.Scheme) error { 52 | scheme.AddKnownTypes(GroupVersion, 53 | &CEosLabDevice{}, 54 | &CEosLabDeviceList{}, 55 | ) 56 | metav1.AddToGroupVersion(scheme, GroupVersion) 57 | metav1.AddMetaToScheme(scheme) 58 | return nil 59 | } 60 | 61 | func init() { 62 | if err := AddToScheme(scheme.Scheme); err != nil { 63 | panic(err) 64 | } 65 | } 66 | -------------------------------------------------------------------------------- /test/reconcile-certs/01-assert.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: v1 2 | kind: Pod 3 | metadata: 4 | labels: 5 | app: ceoslab-device 6 | name: ceoslab-device 7 | spec: 8 | containers: 9 | - volumeMounts: 10 | - mountPath: /mnt/flash/rc.eos 11 | name: volume-configmap-rceos-ceoslab-device 12 | subPath: rc.eos 13 | - mountPath: /mnt/flash/test-cert 14 | name: volume-secret-selfsigned-ceoslab-device-0 15 | subPath: test-cert 16 | - mountPath: /mnt/flash/test-key 17 | name: volume-secret-selfsigned-ceoslab-device-0 18 | subPath: test-key 19 | - mountPath: /mnt/flash/test-cert-2 20 | name: volume-secret-selfsigned-ceoslab-device-1 21 | subPath: test-cert-2 22 | - mountPath: /mnt/flash/test-key-2 23 | name: volume-secret-selfsigned-ceoslab-device-1 24 | subPath: test-key-2 25 | - mountPath: /var/run/secrets/kubernetes.io/serviceaccount 26 | name: ceos 27 | status: 28 | containerStatuses: 29 | - name: ceos 30 | ready: true 31 | --- 32 | apiVersion: ceoslab.arista.com/v1alpha1 33 | kind: CEosLabDevice 34 | metadata: 35 | name: ceoslab-device 36 | status: 37 | configmapconfig: 38 | selfsignedcertstatus: 39 | secret-selfsigned-ceoslab-device-0: 40 | certname: test-cert 41 | commonname: ceoslab-device 42 | keyname: test-key 43 | keysize: 4096 44 | secret-selfsigned-ceoslab-device-1: 45 | certname: test-cert-2 46 | commonname: ceoslab-device 47 | keyname: test-key-2 48 | keysize: 4096 49 | podconfigmapconfig: 50 | selfsignedcertstatus: 51 | secret-selfsigned-ceoslab-device-0: 52 | certname: test-cert 53 | commonname: ceoslab-device 54 | keyname: test-key 55 | keysize: 4096 56 | secret-selfsigned-ceoslab-device-1: 57 | certname: test-cert-2 58 | commonname: ceoslab-device 59 | keyname: test-key-2 60 | keysize: 4096 61 | status: success 62 | --- 63 | apiVersion: v1 64 | kind: Secret 65 | metadata: 66 | name: secret-selfsigned-ceoslab-device-0 67 | ownerReferences: 68 | - apiVersion: ceoslab.arista.com/v1alpha1 69 | blockOwnerDeletion: true 70 | controller: true 71 | kind: CEosLabDevice 72 | name: ceoslab-device 73 | --- 74 | apiVersion: v1 75 | kind: Secret 76 | metadata: 77 | name: secret-selfsigned-ceoslab-device-1 78 | ownerReferences: 79 | - apiVersion: ceoslab.arista.com/v1alpha1 80 | blockOwnerDeletion: true 81 | controller: true 82 | kind: CEosLabDevice 83 | name: ceoslab-device 84 | -------------------------------------------------------------------------------- /controllers/suite_test.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2022. 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | 17 | package controllers 18 | 19 | import ( 20 | "path/filepath" 21 | "testing" 22 | 23 | . "github.com/onsi/ginkgo" 24 | . "github.com/onsi/gomega" 25 | "k8s.io/client-go/kubernetes/scheme" 26 | "k8s.io/client-go/rest" 27 | "sigs.k8s.io/controller-runtime/pkg/client" 28 | "sigs.k8s.io/controller-runtime/pkg/envtest" 29 | "sigs.k8s.io/controller-runtime/pkg/envtest/printer" 30 | logf "sigs.k8s.io/controller-runtime/pkg/log" 31 | "sigs.k8s.io/controller-runtime/pkg/log/zap" 32 | 33 | ceoslabv1alpha1 "github.com/aristanetworks/arista-ceoslab-operator/v2/api/v1alpha1" 34 | //+kubebuilder:scaffold:imports 35 | ) 36 | 37 | // These tests use Ginkgo (BDD-style Go testing framework). Refer to 38 | // http://onsi.github.io/ginkgo/ to learn more about Ginkgo. 39 | 40 | var cfg *rest.Config 41 | var k8sClient client.Client 42 | var testEnv *envtest.Environment 43 | 44 | func TestAPIs(t *testing.T) { 45 | RegisterFailHandler(Fail) 46 | 47 | RunSpecsWithDefaultAndCustomReporters(t, 48 | "Controller Suite", 49 | []Reporter{printer.NewlineReporter{}}) 50 | } 51 | 52 | var _ = BeforeSuite(func() { 53 | logf.SetLogger(zap.New(zap.WriteTo(GinkgoWriter), zap.UseDevMode(true))) 54 | 55 | By("bootstrapping test environment") 56 | testEnv = &envtest.Environment{ 57 | CRDDirectoryPaths: []string{filepath.Join("..", "config", "crd", "bases")}, 58 | ErrorIfCRDPathMissing: true, 59 | } 60 | 61 | var err error 62 | // cfg is defined in this file globally. 63 | cfg, err = testEnv.Start() 64 | Expect(err).NotTo(HaveOccurred()) 65 | Expect(cfg).NotTo(BeNil()) 66 | 67 | err = ceoslabv1alpha1.AddToScheme(scheme.Scheme) 68 | Expect(err).NotTo(HaveOccurred()) 69 | 70 | //+kubebuilder:scaffold:scheme 71 | 72 | k8sClient, err = client.New(cfg, client.Options{Scheme: scheme.Scheme}) 73 | Expect(err).NotTo(HaveOccurred()) 74 | Expect(k8sClient).NotTo(BeNil()) 75 | 76 | }, 60) 77 | 78 | var _ = AfterSuite(func() { 79 | By("tearing down the test environment") 80 | err := testEnv.Stop() 81 | Expect(err).NotTo(HaveOccurred()) 82 | }) 83 | -------------------------------------------------------------------------------- /config/default/kustomization.yaml: -------------------------------------------------------------------------------- 1 | # Adds namespace to all resources. 2 | namespace: arista-ceoslab-operator-system 3 | 4 | # Value of this field is prepended to the 5 | # names of all resources, e.g. a deployment named 6 | # "wordpress" becomes "alices-wordpress". 7 | # Note that it should also match with the prefix (text before '-') of the namespace 8 | # field above. 9 | namePrefix: arista-ceoslab-operator- 10 | 11 | # Labels to add to all resources and selectors. 12 | #commonLabels: 13 | # someName: someValue 14 | 15 | bases: 16 | - ../crd 17 | - ../rbac 18 | - ../manager 19 | # [WEBHOOK] To enable webhook, uncomment all the sections with [WEBHOOK] prefix including the one in 20 | # crd/kustomization.yaml 21 | #- ../webhook 22 | # [CERTMANAGER] To enable cert-manager, uncomment all sections with 'CERTMANAGER'. 'WEBHOOK' components are required. 23 | #- ../certmanager 24 | # [PROMETHEUS] To enable prometheus monitor, uncomment all sections with 'PROMETHEUS'. 25 | #- ../prometheus 26 | 27 | patchesStrategicMerge: 28 | # Protect the /metrics endpoint by putting it behind auth. 29 | # If you want your controller-manager to expose the /metrics 30 | # endpoint w/o any authn/z, please comment the following line. 31 | - manager_auth_proxy_patch.yaml 32 | 33 | # Mount the controller config file for loading manager configurations 34 | # through a ComponentConfig type 35 | #- manager_config_patch.yaml 36 | 37 | # [WEBHOOK] To enable webhook, uncomment all the sections with [WEBHOOK] prefix including the one in 38 | # crd/kustomization.yaml 39 | #- manager_webhook_patch.yaml 40 | 41 | # [CERTMANAGER] To enable cert-manager, uncomment all sections with 'CERTMANAGER'. 42 | # Uncomment 'CERTMANAGER' sections in crd/kustomization.yaml to enable the CA injection in the admission webhooks. 43 | # 'CERTMANAGER' needs to be enabled to use ca injection 44 | #- webhookcainjection_patch.yaml 45 | 46 | # the following config is for teaching kustomize how to do var substitution 47 | vars: 48 | # [CERTMANAGER] To enable cert-manager, uncomment all sections with 'CERTMANAGER' prefix. 49 | #- name: CERTIFICATE_NAMESPACE # namespace of the certificate CR 50 | # objref: 51 | # kind: Certificate 52 | # group: cert-manager.io 53 | # version: v1 54 | # name: serving-cert # this name should match the one in certificate.yaml 55 | # fieldref: 56 | # fieldpath: metadata.namespace 57 | #- name: CERTIFICATE_NAME 58 | # objref: 59 | # kind: Certificate 60 | # group: cert-manager.io 61 | # version: v1 62 | # name: serving-cert # this name should match the one in certificate.yaml 63 | #- name: SERVICE_NAMESPACE # namespace of the service 64 | # objref: 65 | # kind: Service 66 | # version: v1 67 | # name: webhook-service 68 | # fieldref: 69 | # fieldpath: metadata.namespace 70 | #- name: SERVICE_NAME 71 | # objref: 72 | # kind: Service 73 | # version: v1 74 | # name: webhook-service 75 | -------------------------------------------------------------------------------- /.github/workflows/docker-publish.yml: -------------------------------------------------------------------------------- 1 | name: Docker 2 | 3 | # This workflow uses actions that are not certified by GitHub. 4 | # They are provided by a third-party and are governed by 5 | # separate terms of service, privacy policy, and support 6 | # documentation. 7 | 8 | on: 9 | release: 10 | types: 11 | - published 12 | 13 | env: 14 | REGISTRY: ghcr.io 15 | IMAGE_NAME: ${{ github.repository }} 16 | 17 | jobs: 18 | 19 | build: 20 | 21 | runs-on: ubuntu-latest 22 | 23 | permissions: 24 | contents: read 25 | packages: write 26 | # This is used to complete the identity challenge 27 | # with sigstore/fulcio when running outside of PRs. 28 | id-token: write 29 | 30 | steps: 31 | - name: Checkout repository 32 | uses: actions/checkout@v3 33 | 34 | # Install the cosign tool 35 | # https://github.com/sigstore/cosign-installer 36 | - name: Install cosign 37 | uses: sigstore/cosign-installer@59acb6260d9c0ba8f4a2f9d9b48431a222b68e20 38 | with: 39 | cosign-release: 'v2.2.4' 40 | 41 | # Workaround: https://github.com/docker/build-push-action/issues/461 42 | - name: Setup Docker buildx 43 | uses: docker/setup-buildx-action@d70bba72b1f3fd22344832f00baa16ece964efeb 44 | 45 | # Login against a Docker registry 46 | # https://github.com/docker/login-action 47 | - name: Log into registry ${{ env.REGISTRY }} 48 | uses: docker/login-action@e92390c5fb421da1463c202d546fed0ec5c39f20 49 | with: 50 | registry: ${{ env.REGISTRY }} 51 | username: ${{ github.actor }} 52 | password: ${{ secrets.GITHUB_TOKEN }} 53 | 54 | # Extract metadata (tags, labels) for Docker 55 | # https://github.com/docker/metadata-action 56 | - name: Extract Docker metadata 57 | id: meta 58 | uses: docker/metadata-action@8e5442c4ef9f78752691e2d8f8d19755c6f78e81 59 | with: 60 | images: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }} 61 | 62 | # Build and push Docker image with Buildx (don't push on PR) 63 | # https://github.com/docker/build-push-action 64 | - name: Build and push Docker image 65 | id: build-and-push 66 | uses: docker/build-push-action@2cdde995de11925a030ce8070c3d77a52ffcf1c0 67 | with: 68 | context: . 69 | push: true 70 | tags: ${{ steps.meta.outputs.tags }} 71 | 72 | # Sign the resulting Docker image digest except on PRs. 73 | # This will only write to the public Rekor transparency log when the Docker 74 | # repository is public to avoid leaking data. If you would like to publish 75 | # transparency data even for private images, pass --force to cosign below. 76 | # https://github.com/sigstore/cosign 77 | - name: Sign the published Docker image 78 | env: 79 | COSIGN_EXPERIMENTAL: "true" 80 | # This step uses the identity token to provision an ephemeral certificate 81 | # against the sigstore community Fulcio instance. 82 | run: echo "${{ steps.meta.outputs.tags }}" | xargs -I {} cosign sign --yes {}@${{ steps.build-and-push.outputs.digest }} 83 | -------------------------------------------------------------------------------- /main.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2022. 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | 17 | package main 18 | 19 | import ( 20 | "flag" 21 | "os" 22 | 23 | // Import all Kubernetes client auth plugins (e.g. Azure, GCP, OIDC, etc.) 24 | // to ensure that exec-entrypoint and run can make use of them. 25 | _ "k8s.io/client-go/plugin/pkg/client/auth" 26 | 27 | "k8s.io/apimachinery/pkg/runtime" 28 | utilruntime "k8s.io/apimachinery/pkg/util/runtime" 29 | clientgoscheme "k8s.io/client-go/kubernetes/scheme" 30 | ctrl "sigs.k8s.io/controller-runtime" 31 | "sigs.k8s.io/controller-runtime/pkg/healthz" 32 | "sigs.k8s.io/controller-runtime/pkg/log/zap" 33 | 34 | ceoslabv1alpha1 "github.com/aristanetworks/arista-ceoslab-operator/v2/api/v1alpha1" 35 | "github.com/aristanetworks/arista-ceoslab-operator/v2/controllers" 36 | //+kubebuilder:scaffold:imports 37 | ) 38 | 39 | var ( 40 | scheme = runtime.NewScheme() 41 | setupLog = ctrl.Log.WithName("setup") 42 | ) 43 | 44 | func init() { 45 | utilruntime.Must(clientgoscheme.AddToScheme(scheme)) 46 | 47 | utilruntime.Must(ceoslabv1alpha1.AddToScheme(scheme)) 48 | //+kubebuilder:scaffold:scheme 49 | } 50 | 51 | func main() { 52 | var metricsAddr string 53 | var enableLeaderElection bool 54 | var probeAddr string 55 | flag.StringVar(&metricsAddr, "metrics-bind-address", ":8080", "The address the metric endpoint binds to.") 56 | flag.StringVar(&probeAddr, "health-probe-bind-address", ":8081", "The address the probe endpoint binds to.") 57 | flag.BoolVar(&enableLeaderElection, "leader-elect", false, 58 | "Enable leader election for controller manager. "+ 59 | "Enabling this will ensure there is only one active controller manager.") 60 | opts := zap.Options{ 61 | Development: true, 62 | } 63 | opts.BindFlags(flag.CommandLine) 64 | flag.Parse() 65 | 66 | ctrl.SetLogger(zap.New(zap.UseFlagOptions(&opts))) 67 | 68 | mgr, err := ctrl.NewManager(ctrl.GetConfigOrDie(), ctrl.Options{ 69 | Scheme: scheme, 70 | MetricsBindAddress: metricsAddr, 71 | Port: 9443, 72 | HealthProbeBindAddress: probeAddr, 73 | LeaderElection: enableLeaderElection, 74 | LeaderElectionID: "a8645112.arista.com", 75 | }) 76 | if err != nil { 77 | setupLog.Error(err, "unable to start manager") 78 | os.Exit(1) 79 | } 80 | 81 | if err = (&controllers.CEosLabDeviceReconciler{ 82 | Client: mgr.GetClient(), 83 | Scheme: mgr.GetScheme(), 84 | }).SetupWithManager(mgr); err != nil { 85 | setupLog.Error(err, "unable to create controller", "controller", "CEosLabDevice") 86 | os.Exit(1) 87 | } 88 | //+kubebuilder:scaffold:builder 89 | 90 | if err := mgr.AddHealthzCheck("healthz", healthz.Ping); err != nil { 91 | setupLog.Error(err, "unable to set up health check") 92 | os.Exit(1) 93 | } 94 | if err := mgr.AddReadyzCheck("readyz", healthz.Ping); err != nil { 95 | setupLog.Error(err, "unable to set up ready check") 96 | os.Exit(1) 97 | } 98 | 99 | setupLog.Info("starting manager") 100 | if err := mgr.Start(ctrl.SetupSignalHandler()); err != nil { 101 | setupLog.Error(err, "problem running manager") 102 | os.Exit(1) 103 | } 104 | } 105 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/aristanetworks/arista-ceoslab-operator/v2 2 | 3 | go 1.17 4 | 5 | require ( 6 | github.com/go-logr/logr v1.2.3 7 | github.com/onsi/ginkgo v1.16.5 8 | github.com/onsi/gomega v1.18.1 9 | k8s.io/api v0.24.3 10 | k8s.io/apimachinery v0.24.3 11 | k8s.io/client-go v0.24.3 12 | k8s.io/utils v0.0.0-20220823124924-e9cbc92d1a73 13 | sigs.k8s.io/controller-runtime v0.12.2 14 | ) 15 | 16 | require ( 17 | cloud.google.com/go v0.81.0 // indirect 18 | github.com/Azure/go-autorest v14.2.0+incompatible // indirect 19 | github.com/Azure/go-autorest/autorest v0.11.18 // indirect 20 | github.com/Azure/go-autorest/autorest/adal v0.9.13 // indirect 21 | github.com/Azure/go-autorest/autorest/date v0.3.0 // indirect 22 | github.com/Azure/go-autorest/logger v0.2.1 // indirect 23 | github.com/Azure/go-autorest/tracing v0.6.0 // indirect 24 | github.com/PuerkitoBio/purell v1.1.1 // indirect 25 | github.com/PuerkitoBio/urlesc v0.0.0-20170810143723-de5bf2ad4578 // indirect 26 | github.com/beorn7/perks v1.0.1 // indirect 27 | github.com/cespare/xxhash/v2 v2.1.2 // indirect 28 | github.com/davecgh/go-spew v1.1.1 // indirect 29 | github.com/emicklei/go-restful v2.9.5+incompatible // indirect 30 | github.com/evanphx/json-patch v4.12.0+incompatible // indirect 31 | github.com/form3tech-oss/jwt-go v3.2.3+incompatible // indirect 32 | github.com/fsnotify/fsnotify v1.5.1 // indirect 33 | github.com/go-logr/zapr v1.2.0 // indirect 34 | github.com/go-openapi/jsonpointer v0.19.5 // indirect 35 | github.com/go-openapi/jsonreference v0.19.5 // indirect 36 | github.com/go-openapi/swag v0.19.14 // indirect 37 | github.com/gogo/protobuf v1.3.2 // indirect 38 | github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da // indirect 39 | github.com/golang/protobuf v1.5.2 // indirect 40 | github.com/google/gnostic v0.5.7-v3refs // indirect 41 | github.com/google/go-cmp v0.5.5 // indirect 42 | github.com/google/gofuzz v1.1.0 // indirect 43 | github.com/google/uuid v1.1.2 // indirect 44 | github.com/imdario/mergo v0.3.12 // indirect 45 | github.com/josharian/intern v1.0.0 // indirect 46 | github.com/json-iterator/go v1.1.12 // indirect 47 | github.com/mailru/easyjson v0.7.6 // indirect 48 | github.com/matttproud/golang_protobuf_extensions v1.0.2-0.20181231171920-c182affec369 // indirect 49 | github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect 50 | github.com/modern-go/reflect2 v1.0.2 // indirect 51 | github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect 52 | github.com/nxadm/tail v1.4.8 // indirect 53 | github.com/pkg/errors v0.9.1 // indirect 54 | github.com/prometheus/client_golang v1.12.1 // indirect 55 | github.com/prometheus/client_model v0.2.0 // indirect 56 | github.com/prometheus/common v0.32.1 // indirect 57 | github.com/prometheus/procfs v0.7.3 // indirect 58 | github.com/spf13/pflag v1.0.5 // indirect 59 | go.uber.org/atomic v1.7.0 // indirect 60 | go.uber.org/multierr v1.6.0 // indirect 61 | go.uber.org/zap v1.19.1 // indirect 62 | golang.org/x/crypto v0.0.0-20220214200702-86341886e292 // indirect 63 | golang.org/x/net v0.0.0-20220127200216-cd36cc0744dd // indirect 64 | golang.org/x/oauth2 v0.0.0-20211104180415-d3ed0bb246c8 // indirect 65 | golang.org/x/sys v0.0.0-20220209214540-3681064d5158 // indirect 66 | golang.org/x/term v0.0.0-20210927222741-03fcf44c2211 // indirect 67 | golang.org/x/text v0.3.7 // indirect 68 | golang.org/x/time v0.0.0-20220210224613-90d013bbcef8 // indirect 69 | gomodules.xyz/jsonpatch/v2 v2.2.0 // indirect 70 | google.golang.org/appengine v1.6.7 // indirect 71 | google.golang.org/protobuf v1.27.1 // indirect 72 | gopkg.in/inf.v0 v0.9.1 // indirect 73 | gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7 // indirect 74 | gopkg.in/yaml.v2 v2.4.0 // indirect 75 | gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b // indirect 76 | k8s.io/apiextensions-apiserver v0.24.2 // indirect 77 | k8s.io/component-base v0.24.2 // indirect 78 | k8s.io/klog/v2 v2.60.1 // indirect 79 | k8s.io/kube-openapi v0.0.0-20220328201542-3ee0da9b0b42 // indirect 80 | sigs.k8s.io/json v0.0.0-20211208200746-9f7c6b3444d2 // indirect 81 | sigs.k8s.io/structured-merge-diff/v4 v4.2.1 // indirect 82 | sigs.k8s.io/yaml v1.3.0 // indirect 83 | ) 84 | -------------------------------------------------------------------------------- /api/v1alpha1/clientset/client.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2022. 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | 17 | package clientset 18 | 19 | import ( 20 | "context" 21 | 22 | "github.com/aristanetworks/arista-ceoslab-operator/v2/api/v1alpha1" 23 | intf "github.com/aristanetworks/arista-ceoslab-operator/v2/api/v1alpha1/client" 24 | metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" 25 | "k8s.io/apimachinery/pkg/types" 26 | "k8s.io/apimachinery/pkg/watch" 27 | "k8s.io/client-go/kubernetes/scheme" 28 | "k8s.io/client-go/rest" 29 | ) 30 | 31 | type CEosLabDeviceV1Alpha1Client struct { 32 | restClient rest.Interface 33 | } 34 | 35 | func NewForConfig(c *rest.Config) (*CEosLabDeviceV1Alpha1Client, error) { 36 | config := *c 37 | config.ContentConfig.GroupVersion = &v1alpha1.GroupVersion 38 | config.APIPath = "/apis" 39 | config.NegotiatedSerializer = scheme.Codecs.WithoutConversion() 40 | config.UserAgent = rest.DefaultKubernetesUserAgent() 41 | 42 | client, err := rest.RESTClientFor(&config) 43 | if err != nil { 44 | return nil, err 45 | } 46 | 47 | return &CEosLabDeviceV1Alpha1Client{restClient: client}, nil 48 | } 49 | 50 | func (c *CEosLabDeviceV1Alpha1Client) CEosLabDevices(namespace string) intf.CEosLabDeviceOpsInterface { 51 | return &ceosLabDeviceClient{ 52 | restClient: c.restClient, 53 | ns: namespace, 54 | } 55 | } 56 | 57 | type ceosLabDeviceClient struct { 58 | restClient rest.Interface 59 | ns string 60 | } 61 | 62 | var ( 63 | _ intf.CEosLabDeviceV1Alpha1Interface = (*CEosLabDeviceV1Alpha1Client)(nil) 64 | _ intf.CEosLabDeviceOpsInterface = (*ceosLabDeviceClient)(nil) 65 | ) 66 | 67 | func resource() string { 68 | return v1alpha1.GroupVersionResource.Resource 69 | } 70 | 71 | func (c *ceosLabDeviceClient) List(ctx context.Context, opts metav1.ListOptions) (*v1alpha1.CEosLabDeviceList, error) { 72 | result := v1alpha1.CEosLabDeviceList{} 73 | err := c.restClient. 74 | Get(). 75 | Namespace(c.ns). 76 | Resource(resource()). 77 | VersionedParams(&opts, scheme.ParameterCodec). 78 | Do(ctx). 79 | Into(&result) 80 | return &result, err 81 | } 82 | 83 | func (c *ceosLabDeviceClient) Get(ctx context.Context, name string, opts metav1.GetOptions) (*v1alpha1.CEosLabDevice, error) { 84 | result := v1alpha1.CEosLabDevice{} 85 | err := c.restClient. 86 | Get(). 87 | Namespace(c.ns). 88 | Resource(resource()). 89 | Name(name). 90 | VersionedParams(&opts, scheme.ParameterCodec). 91 | Do(ctx). 92 | Into(&result) 93 | return &result, err 94 | } 95 | 96 | func (c *ceosLabDeviceClient) Create(ctx context.Context, device *v1alpha1.CEosLabDevice, opts metav1.CreateOptions) (*v1alpha1.CEosLabDevice, error) { 97 | result := v1alpha1.CEosLabDevice{} 98 | err := c.restClient. 99 | Post(). 100 | Namespace(c.ns). 101 | Resource(resource()). 102 | VersionedParams(&opts, scheme.ParameterCodec). 103 | Body(device). 104 | Do(ctx). 105 | Into(&result) 106 | return &result, err 107 | } 108 | 109 | func (c *ceosLabDeviceClient) Watch(ctx context.Context, opts metav1.ListOptions) (watch.Interface, error) { 110 | opts.Watch = true 111 | return c.restClient. 112 | Get(). 113 | Namespace(c.ns). 114 | Resource(resource()). 115 | VersionedParams(&opts, scheme.ParameterCodec). 116 | Watch(ctx) 117 | } 118 | 119 | func (c *ceosLabDeviceClient) Delete(ctx context.Context, name string, opts metav1.DeleteOptions) error { 120 | err := c.restClient. 121 | Delete(). 122 | Namespace(c.ns). 123 | Resource(resource()). 124 | Name(name). 125 | VersionedParams(&opts, scheme.ParameterCodec). 126 | Do(ctx). 127 | Error() 128 | return err 129 | } 130 | 131 | func (c *ceosLabDeviceClient) Patch(ctx context.Context, name string, pt types.PatchType, data []byte, opts metav1.PatchOptions, subresources ...string) (result *v1alpha1.CEosLabDevice, err error) { 132 | result = &v1alpha1.CEosLabDevice{} 133 | err = c.restClient. 134 | Patch(pt). 135 | Namespace(c.ns). 136 | Resource(resource()). 137 | Name(name). 138 | SubResource(subresources...). 139 | VersionedParams(&opts, scheme.ParameterCodec). 140 | Body(data). 141 | Do(ctx). 142 | Into(result) 143 | return 144 | } 145 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | # VERSION defines the project version for the bundle. 2 | # Update this value when you upgrade the version of your project. 3 | # To re-generate a bundle for another specific version without changing the standard setup, you can: 4 | # - use the VERSION as arg of the bundle target (e.g make bundle VERSION=0.0.2) 5 | # - use environment variables to overwrite this value (e.g export VERSION=0.0.2) 6 | VERSION ?= 2.1.2 7 | 8 | # Image URL to use all building/pushing image targets 9 | IMG ?= ghcr.io/aristanetworks/arista-ceoslab-operator 10 | TAGGED_IMG := $(IMG):v$(VERSION) 11 | 12 | # Get the currently used golang install path (in GOPATH/bin, unless GOBIN is set) 13 | ifeq (,$(shell go env GOBIN)) 14 | GOBIN=$(shell go env GOPATH)/bin 15 | else 16 | GOBIN=$(shell go env GOBIN) 17 | endif 18 | 19 | # Setting SHELL to bash allows bash commands to be executed by recipes. 20 | # This is a requirement for 'setup-envtest.sh' in the test target. 21 | # Options are set to exit when a recipe line exits non-zero or a piped command fails. 22 | SHELL = /usr/bin/env bash -o pipefail 23 | .SHELLFLAGS = -ec 24 | 25 | .PHONY: all 26 | all: build 27 | 28 | ##@ General 29 | 30 | # The help target prints out all targets with their descriptions organized 31 | # beneath their categories. The categories are represented by '##@' and the 32 | # target descriptions by '##'. The awk commands is responsible for reading the 33 | # entire set of makefiles included in this invocation, looking for lines of the 34 | # file as xyz: ## something, and then pretty-format the target and help. Then, 35 | # if there's a line with ##@ something, that gets pretty-printed as a category. 36 | # More info on the usage of ANSI control characters for terminal formatting: 37 | # https://en.wikipedia.org/wiki/ANSI_escape_code#SGR_parameters 38 | # More info on the awk command: 39 | # http://linuxcommand.org/lc3_adv_awk.php 40 | 41 | .PHONY: help 42 | help: ## Display this help. 43 | @awk 'BEGIN {FS = ":.*##"; printf "\nUsage:\n make \033[36m\033[0m\n"} /^[a-zA-Z_0-9-]+:.*?##/ { printf " \033[36m%-15s\033[0m %s\n", $$1, $$2 } /^##@/ { printf "\n\033[1m%s\033[0m\n", substr($$0, 5) } ' $(MAKEFILE_LIST) 44 | 45 | ##@ Development 46 | 47 | .PHONY: manifests 48 | manifests: controller-gen kustomize ## Generate WebhookConfiguration, ClusterRole and CustomResourceDefinition objects. 49 | $(CONTROLLER_GEN) rbac:roleName=manager-role crd webhook paths="./..." output:crd:artifacts:config=config/crd/bases 50 | cd config/manager && $(KUSTOMIZE) edit set image controller=$(TAGGED_IMG) 51 | $(KUSTOMIZE) build config/default > config/kustomized/manifest.yaml 52 | 53 | .PHONY: generate 54 | generate: controller-gen ## Generate code containing DeepCopy, DeepCopyInto, and DeepCopyObject method implementations. 55 | $(CONTROLLER_GEN) object:headerFile="hack/boilerplate.go.txt" paths="./..." 56 | 57 | .PHONY: fmt 58 | fmt: ## Run go fmt against code. 59 | go fmt ./... 60 | 61 | .PHONY: vet 62 | vet: ## Run go vet against code. 63 | go vet ./... 64 | 65 | ##@ Build 66 | 67 | .PHONY: build 68 | build: generate fmt vet ## Build manager binary. 69 | go build -o bin/manager main.go 70 | 71 | .PHONY: run 72 | run: manifests generate fmt vet ## Run a controller from your host. 73 | go run ./main.go 74 | 75 | .PHONY: docker-build 76 | docker-build: test ## Build docker image with the manager. 77 | docker build -t ${TAGGED_IMG} -t ${IMG}:latest . 78 | 79 | .PHONY: docker-push 80 | docker-push: ## Push docker image with the manager. 81 | docker push ${TAGGED_IMG} -t ${IMG}:latest 82 | 83 | ##@ Deployment 84 | 85 | ifndef ignore-not-found 86 | ignore-not-found = false 87 | endif 88 | 89 | .PHONY: install 90 | install: manifests kustomize ## Install CRDs into the K8s cluster specified in ~/.kube/config. 91 | $(KUSTOMIZE) build config/crd | kubectl apply -f - 92 | 93 | .PHONY: uninstall 94 | uninstall: manifests kustomize ## Uninstall CRDs from the K8s cluster specified in ~/.kube/config. Call with ignore-not-found=true to ignore resource not found errors during deletion. 95 | $(KUSTOMIZE) build config/crd | kubectl delete --ignore-not-found=$(ignore-not-found) -f - 96 | 97 | .PHONY: deploy 98 | deploy: manifests kustomize ## Deploy controller to the K8s cluster specified in ~/.kube/config. 99 | cd config/manager && $(KUSTOMIZE) edit set image controller=${TAGGED_IMG} 100 | $(KUSTOMIZE) build config/default | kubectl apply -f - 101 | 102 | .PHONY: undeploy 103 | undeploy: ## Undeploy controller from the K8s cluster specified in ~/.kube/config. Call with ignore-not-found=true to ignore resource not found errors during deletion. 104 | $(KUSTOMIZE) build config/default | kubectl delete --ignore-not-found=$(ignore-not-found) -f - 105 | 106 | ##@ Build Dependencies 107 | 108 | ## Location to install dependencies to 109 | LOCALBIN ?= $(shell pwd)/bin 110 | $(LOCALBIN): 111 | mkdir -p $(LOCALBIN) 112 | 113 | ## Tool Binaries 114 | KUSTOMIZE ?= $(LOCALBIN)/kustomize 115 | CONTROLLER_GEN ?= $(LOCALBIN)/controller-gen 116 | ENVTEST ?= $(LOCALBIN)/setup-envtest 117 | 118 | ## Tool Versions 119 | KUSTOMIZE_VERSION ?= v3.8.7 120 | CONTROLLER_TOOLS_VERSION ?= v0.8.0 121 | 122 | KUSTOMIZE_INSTALL_SCRIPT ?= "https://raw.githubusercontent.com/kubernetes-sigs/kustomize/master/hack/install_kustomize.sh" 123 | .PHONY: kustomize 124 | kustomize: $(KUSTOMIZE) ## Download kustomize locally if necessary. 125 | $(KUSTOMIZE): $(LOCALBIN) 126 | curl -s $(KUSTOMIZE_INSTALL_SCRIPT) | bash -s -- $(subst v,,$(KUSTOMIZE_VERSION)) $(LOCALBIN) 127 | 128 | .PHONY: controller-gen 129 | controller-gen: $(CONTROLLER_GEN) ## Download controller-gen locally if necessary. 130 | $(CONTROLLER_GEN): $(LOCALBIN) 131 | GOBIN=$(LOCALBIN) go install sigs.k8s.io/controller-tools/cmd/controller-gen@$(CONTROLLER_TOOLS_VERSION) 132 | -------------------------------------------------------------------------------- /api/v1alpha1/dynamic/client.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2022. 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | 17 | package dynamic 18 | 19 | import ( 20 | "context" 21 | "fmt" 22 | 23 | "github.com/aristanetworks/arista-ceoslab-operator/v2/api/v1alpha1" 24 | intf "github.com/aristanetworks/arista-ceoslab-operator/v2/api/v1alpha1/client" 25 | metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" 26 | "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" 27 | "k8s.io/apimachinery/pkg/runtime" 28 | "k8s.io/apimachinery/pkg/types" 29 | "k8s.io/apimachinery/pkg/watch" 30 | "k8s.io/client-go/dynamic" 31 | "k8s.io/client-go/kubernetes/scheme" 32 | "k8s.io/client-go/rest" 33 | ) 34 | 35 | type CEosLabDeviceV1Alpha1Client struct { 36 | dynamicResource dynamic.NamespaceableResourceInterface 37 | } 38 | 39 | // NewForConfig returns a new Clientset based on c. 40 | func NewForConfig(c *rest.Config) (*CEosLabDeviceV1Alpha1Client, error) { 41 | config := *c 42 | config.ContentConfig.GroupVersion = &v1alpha1.GroupVersion 43 | config.APIPath = "/apis" 44 | config.NegotiatedSerializer = scheme.Codecs.WithoutConversion() 45 | config.UserAgent = rest.DefaultKubernetesUserAgent() 46 | 47 | client, err := dynamic.NewForConfig(c) 48 | if err != nil { 49 | return nil, err 50 | } 51 | resourceInterface := client.Resource(v1alpha1.GroupVersionResource) 52 | 53 | return &CEosLabDeviceV1Alpha1Client{dynamicResource: resourceInterface}, nil 54 | } 55 | 56 | func (c *CEosLabDeviceV1Alpha1Client) CEosLabDevices(namespace string) intf.CEosLabDeviceOpsInterface { 57 | return &ceosLabDeviceClient{ 58 | dynamicResource: c.dynamicResource, 59 | ns: namespace, 60 | } 61 | } 62 | 63 | type ceosLabDeviceClient struct { 64 | dynamicResource dynamic.NamespaceableResourceInterface 65 | ns string 66 | } 67 | 68 | func (c *CEosLabDeviceV1Alpha1Client) SetDeviceClient(newDynamicResource dynamic.NamespaceableResourceInterface) { 69 | c.dynamicResource = newDynamicResource 70 | } 71 | 72 | var ( 73 | _ intf.CEosLabDeviceV1Alpha1Interface = (*CEosLabDeviceV1Alpha1Client)(nil) 74 | _ intf.CEosLabDeviceOpsInterface = (*ceosLabDeviceClient)(nil) 75 | ) 76 | 77 | func (c *ceosLabDeviceClient) List(ctx context.Context, opts metav1.ListOptions) (*v1alpha1.CEosLabDeviceList, error) { 78 | result := v1alpha1.CEosLabDeviceList{} 79 | u, err := c.dynamicResource.Namespace(c.ns).List(ctx, opts) 80 | if err != nil { 81 | return nil, err 82 | } 83 | if err = runtime.DefaultUnstructuredConverter.FromUnstructured(u.UnstructuredContent(), &result); err != nil { 84 | return nil, fmt.Errorf("failed to type assert return to CEosLabDeviceList: %w", err) 85 | } 86 | return &result, err 87 | } 88 | 89 | func (c *ceosLabDeviceClient) Get(ctx context.Context, name string, opts metav1.GetOptions) (*v1alpha1.CEosLabDevice, error) { 90 | u, err := c.dynamicResource.Namespace(c.ns).Get(ctx, name, opts) 91 | if err != nil { 92 | return nil, err 93 | } 94 | result := v1alpha1.CEosLabDevice{} 95 | if err = runtime.DefaultUnstructuredConverter.FromUnstructured(u.UnstructuredContent(), &result); err != nil { 96 | return nil, fmt.Errorf("failed to type assert return to CEosLabDevice: %w", err) 97 | } 98 | return &result, nil 99 | } 100 | 101 | func (c *ceosLabDeviceClient) Create(ctx context.Context, device *v1alpha1.CEosLabDevice, opts metav1.CreateOptions) (*v1alpha1.CEosLabDevice, error) { 102 | device.TypeMeta = metav1.TypeMeta{ 103 | Kind: v1alpha1.GroupVersionKind.Kind, 104 | APIVersion: v1alpha1.GroupVersion.String(), 105 | } 106 | obj, err := runtime.DefaultUnstructuredConverter.ToUnstructured(device) 107 | if err != nil { 108 | return nil, fmt.Errorf("failed to convert CEosLabDevice to unstructured: %w", err) 109 | } 110 | u, err := c.dynamicResource.Namespace(c.ns).Create(ctx, &unstructured.Unstructured{Object: obj}, opts) 111 | if err != nil { 112 | return nil, err 113 | } 114 | result := v1alpha1.CEosLabDevice{} 115 | if err = runtime.DefaultUnstructuredConverter.FromUnstructured(u.UnstructuredContent(), &result); err != nil { 116 | return nil, fmt.Errorf("failed to type assert return to CEosLabDevice: %w", err) 117 | } 118 | return &result, nil 119 | } 120 | 121 | func (c *ceosLabDeviceClient) Watch(ctx context.Context, opts metav1.ListOptions) (watch.Interface, error) { 122 | opts.Watch = true 123 | return c.dynamicResource.Namespace(c.ns).Watch(ctx, opts) 124 | } 125 | 126 | func (c *ceosLabDeviceClient) Delete(ctx context.Context, name string, opts metav1.DeleteOptions) error { 127 | return c.dynamicResource.Namespace(c.ns).Delete(ctx, name, opts) 128 | } 129 | 130 | func (c *ceosLabDeviceClient) Patch(ctx context.Context, name string, pt types.PatchType, data []byte, opts metav1.PatchOptions, subresources ...string) (*v1alpha1.CEosLabDevice, error) { 131 | u, err := c.dynamicResource.Namespace(c.ns).Patch(ctx, name, pt, data, opts, subresources...) 132 | if err != nil { 133 | return nil, err 134 | } 135 | result := v1alpha1.CEosLabDevice{} 136 | if err = runtime.DefaultUnstructuredConverter.FromUnstructured(u.UnstructuredContent(), &result); err != nil { 137 | return nil, fmt.Errorf("failed to type assert return to CEosLabDevice: %w", err) 138 | } 139 | return &result, nil 140 | } 141 | -------------------------------------------------------------------------------- /api/v1alpha1/ceoslabdevice_types.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2022. 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | 17 | package v1alpha1 18 | 19 | import ( 20 | metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" 21 | ) 22 | 23 | // EDIT THIS FILE! THIS IS SCAFFOLDING FOR YOU TO OWN! 24 | // NOTE: json tags are required. Any new fields you add must have json tags for the fields to be serialized. 25 | 26 | // CEosLabDeviceSpec defines the desired state of CEosLabDevice 27 | type CEosLabDeviceSpec struct { 28 | // INSERT ADDITIONAL SPEC FIELDS - desired state of cluster 29 | // Important: Run "make" to regenerate code after modifying this file 30 | 31 | // Additional environment variables. Those necessary to boot properly are already present. 32 | EnvVar map[string]string `json:"envvars,omitempty"` 33 | // Image name. Default: ceos:latest 34 | Image string `json:"image,omitempty"` 35 | // Additional arguments to pass to /sbin/init. Those necessary to boot properly are already present. 36 | Args []string `json:"args,omitempty"` 37 | // Resource requests to configure on the pod. Default: none 38 | Resources map[string]string `json:"resourcerequirements,omitempty"` 39 | // Port mappings for container services. Default: none 40 | Services map[string]ServiceConfig `json:"services,omitempty"` 41 | // Init container image name. Default: networkop/init-wait:latest 42 | InitContainerImage string `json:"initcontainerimage,omitempty"` 43 | // Number of data interfaces to create. An additional interface (eth0) is created for pod connectivity. Default: 0 interfaces 44 | NumInterfaces int32 `json:"numinterfaces,omitempty"` 45 | // Time (in seconds) to wait before starting the device. Default: 0 seconds 46 | Sleep int32 `json:"sleep,omitempty"` 47 | // X.509 certificate configuration. 48 | CertConfig CertConfig `json:"certconfig,omitempty"` 49 | // Explicit interface mapping between kernel devices and interface names. If this is defined, any unmapped devices are ignored. 50 | IntfMapping map[string]string `json:"intfmapping,omitempty"` 51 | // EOS feature toggle overrides 52 | ToggleOverrides map[string]bool `json:"toggleoverrides,omitempty"` 53 | // EOS agents to for the startup probe to block on 54 | WaitForAgents []string `json:"waitforagents,omitempty"` 55 | } 56 | 57 | type ServiceConfig struct { 58 | // TCP ports to forward to the pod. 59 | TCPPorts []PortConfig `json:"tcpports,omitempty"` 60 | } 61 | 62 | type PortConfig struct { 63 | // Port inside the container. 64 | In int32 `json:"in,omitempty"` 65 | // Port outside the container. Defaults to the same as in. 66 | Out int32 `json:"out,omitempty"` 67 | } 68 | 69 | type CertConfig struct { 70 | // Configuration for self-signed certificates. 71 | SelfSignedCerts []SelfSignedCertConfig `json:"selfsignedcerts,omitempty"` 72 | } 73 | 74 | type SelfSignedCertConfig struct { 75 | // Certificate name on the node. 76 | CertName string `json:"certname,omitempty"` 77 | // Key name on the node. 78 | KeyName string `json:"keyname,omitempty"` 79 | // RSA keysize to use for key generation. 80 | KeySize int32 `json:"keysize,omitempty"` 81 | // Common name to set in the cert. 82 | CommonName string `json:"commonname,omitempty"` 83 | } 84 | 85 | // CEosLabDeviceStatus defines the observed state of CEosLabDevice 86 | type CEosLabDeviceStatus struct { 87 | // INSERT ADDITIONAL STATUS FIELD - define observed state of cluster 88 | // Important: Run "make" to regenerate code after modifying this file 89 | 90 | // Device status 91 | State string `json:"status,omitempty"` 92 | // Reason for potential failure 93 | Reason string `json:"reason,omitempty"` 94 | 95 | // It's difficult to deduce the config maps' running state because the data is unstructured. 96 | // For simplicity's sake we store the last configuration here. Other parameters are deduced 97 | // via the K8s API. 98 | 99 | // ConfigMap state as configured in configmaps 100 | ConfigMapStatus ConfigMapStatus `json:"configmapconfig,omitempty"` 101 | // ConfigMap state as present in the pod. If these diverge, we need to restart the pod to 102 | // update. Even if an in-place update is possible these are needed at boot time. 103 | PodConfigMapStatus ConfigMapStatus `json:"podconfigmapconfig,omitempty"` 104 | } 105 | 106 | type ConfigMapStatus struct { 107 | SelfSignedCertStatus map[string]SelfSignedCertConfig `json:"selfsignedcertstatus,omitempty"` 108 | IntfMappingStatus map[string]string `json:"intfmappingstatus,omitempty"` 109 | ToggleOverridesStatus map[string]bool `json:"toggleoverridesstatus,omitempty"` 110 | RcEosStale bool `json:"rceosstale,omitempty"` 111 | StartupConfigResourceVersion *string `json:"startupconfigresourceversion,omitempty"` 112 | } 113 | 114 | //+kubebuilder:object:root=true 115 | //+kubebuilder:subresource:status 116 | 117 | // CEosLabDevice is the Schema for the ceoslabdevices API 118 | type CEosLabDevice struct { 119 | metav1.TypeMeta `json:",inline"` 120 | metav1.ObjectMeta `json:"metadata,omitempty"` 121 | 122 | Spec CEosLabDeviceSpec `json:"spec,omitempty"` 123 | Status CEosLabDeviceStatus `json:"status,omitempty"` 124 | } 125 | 126 | //+kubebuilder:object:root=true 127 | 128 | // CEosLabDeviceList contains a list of CEosLabDevice 129 | type CEosLabDeviceList struct { 130 | metav1.TypeMeta `json:",inline"` 131 | metav1.ListMeta `json:"metadata,omitempty"` 132 | Items []CEosLabDevice `json:"items"` 133 | } 134 | -------------------------------------------------------------------------------- /api/v1alpha1/zz_generated.deepcopy.go: -------------------------------------------------------------------------------- 1 | //go:build !ignore_autogenerated 2 | // +build !ignore_autogenerated 3 | 4 | /* 5 | Copyright 2022. 6 | 7 | Licensed under the Apache License, Version 2.0 (the "License"); 8 | you may not use this file except in compliance with the License. 9 | 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, software 14 | distributed under the License is distributed on an "AS IS" BASIS, 15 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 16 | See the License for the specific language governing permissions and 17 | limitations under the License. 18 | */ 19 | 20 | // Code generated by controller-gen. DO NOT EDIT. 21 | 22 | package v1alpha1 23 | 24 | import ( 25 | "k8s.io/apimachinery/pkg/runtime" 26 | ) 27 | 28 | // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. 29 | func (in *CEosLabDevice) DeepCopyInto(out *CEosLabDevice) { 30 | *out = *in 31 | out.TypeMeta = in.TypeMeta 32 | in.ObjectMeta.DeepCopyInto(&out.ObjectMeta) 33 | in.Spec.DeepCopyInto(&out.Spec) 34 | in.Status.DeepCopyInto(&out.Status) 35 | } 36 | 37 | // DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new CEosLabDevice. 38 | func (in *CEosLabDevice) DeepCopy() *CEosLabDevice { 39 | if in == nil { 40 | return nil 41 | } 42 | out := new(CEosLabDevice) 43 | in.DeepCopyInto(out) 44 | return out 45 | } 46 | 47 | // DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object. 48 | func (in *CEosLabDevice) DeepCopyObject() runtime.Object { 49 | if c := in.DeepCopy(); c != nil { 50 | return c 51 | } 52 | return nil 53 | } 54 | 55 | // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. 56 | func (in *CEosLabDeviceList) DeepCopyInto(out *CEosLabDeviceList) { 57 | *out = *in 58 | out.TypeMeta = in.TypeMeta 59 | in.ListMeta.DeepCopyInto(&out.ListMeta) 60 | if in.Items != nil { 61 | in, out := &in.Items, &out.Items 62 | *out = make([]CEosLabDevice, len(*in)) 63 | for i := range *in { 64 | (*in)[i].DeepCopyInto(&(*out)[i]) 65 | } 66 | } 67 | } 68 | 69 | // DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new CEosLabDeviceList. 70 | func (in *CEosLabDeviceList) DeepCopy() *CEosLabDeviceList { 71 | if in == nil { 72 | return nil 73 | } 74 | out := new(CEosLabDeviceList) 75 | in.DeepCopyInto(out) 76 | return out 77 | } 78 | 79 | // DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object. 80 | func (in *CEosLabDeviceList) DeepCopyObject() runtime.Object { 81 | if c := in.DeepCopy(); c != nil { 82 | return c 83 | } 84 | return nil 85 | } 86 | 87 | // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. 88 | func (in *CEosLabDeviceSpec) DeepCopyInto(out *CEosLabDeviceSpec) { 89 | *out = *in 90 | if in.EnvVar != nil { 91 | in, out := &in.EnvVar, &out.EnvVar 92 | *out = make(map[string]string, len(*in)) 93 | for key, val := range *in { 94 | (*out)[key] = val 95 | } 96 | } 97 | if in.Args != nil { 98 | in, out := &in.Args, &out.Args 99 | *out = make([]string, len(*in)) 100 | copy(*out, *in) 101 | } 102 | if in.Resources != nil { 103 | in, out := &in.Resources, &out.Resources 104 | *out = make(map[string]string, len(*in)) 105 | for key, val := range *in { 106 | (*out)[key] = val 107 | } 108 | } 109 | if in.Services != nil { 110 | in, out := &in.Services, &out.Services 111 | *out = make(map[string]ServiceConfig, len(*in)) 112 | for key, val := range *in { 113 | (*out)[key] = *val.DeepCopy() 114 | } 115 | } 116 | in.CertConfig.DeepCopyInto(&out.CertConfig) 117 | if in.IntfMapping != nil { 118 | in, out := &in.IntfMapping, &out.IntfMapping 119 | *out = make(map[string]string, len(*in)) 120 | for key, val := range *in { 121 | (*out)[key] = val 122 | } 123 | } 124 | if in.ToggleOverrides != nil { 125 | in, out := &in.ToggleOverrides, &out.ToggleOverrides 126 | *out = make(map[string]bool, len(*in)) 127 | for key, val := range *in { 128 | (*out)[key] = val 129 | } 130 | } 131 | if in.WaitForAgents != nil { 132 | in, out := &in.WaitForAgents, &out.WaitForAgents 133 | *out = make([]string, len(*in)) 134 | copy(*out, *in) 135 | } 136 | } 137 | 138 | // DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new CEosLabDeviceSpec. 139 | func (in *CEosLabDeviceSpec) DeepCopy() *CEosLabDeviceSpec { 140 | if in == nil { 141 | return nil 142 | } 143 | out := new(CEosLabDeviceSpec) 144 | in.DeepCopyInto(out) 145 | return out 146 | } 147 | 148 | // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. 149 | func (in *CEosLabDeviceStatus) DeepCopyInto(out *CEosLabDeviceStatus) { 150 | *out = *in 151 | in.ConfigMapStatus.DeepCopyInto(&out.ConfigMapStatus) 152 | in.PodConfigMapStatus.DeepCopyInto(&out.PodConfigMapStatus) 153 | } 154 | 155 | // DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new CEosLabDeviceStatus. 156 | func (in *CEosLabDeviceStatus) DeepCopy() *CEosLabDeviceStatus { 157 | if in == nil { 158 | return nil 159 | } 160 | out := new(CEosLabDeviceStatus) 161 | in.DeepCopyInto(out) 162 | return out 163 | } 164 | 165 | // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. 166 | func (in *CertConfig) DeepCopyInto(out *CertConfig) { 167 | *out = *in 168 | if in.SelfSignedCerts != nil { 169 | in, out := &in.SelfSignedCerts, &out.SelfSignedCerts 170 | *out = make([]SelfSignedCertConfig, len(*in)) 171 | copy(*out, *in) 172 | } 173 | } 174 | 175 | // DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new CertConfig. 176 | func (in *CertConfig) DeepCopy() *CertConfig { 177 | if in == nil { 178 | return nil 179 | } 180 | out := new(CertConfig) 181 | in.DeepCopyInto(out) 182 | return out 183 | } 184 | 185 | // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. 186 | func (in *ConfigMapStatus) DeepCopyInto(out *ConfigMapStatus) { 187 | *out = *in 188 | if in.SelfSignedCertStatus != nil { 189 | in, out := &in.SelfSignedCertStatus, &out.SelfSignedCertStatus 190 | *out = make(map[string]SelfSignedCertConfig, len(*in)) 191 | for key, val := range *in { 192 | (*out)[key] = val 193 | } 194 | } 195 | if in.IntfMappingStatus != nil { 196 | in, out := &in.IntfMappingStatus, &out.IntfMappingStatus 197 | *out = make(map[string]string, len(*in)) 198 | for key, val := range *in { 199 | (*out)[key] = val 200 | } 201 | } 202 | if in.ToggleOverridesStatus != nil { 203 | in, out := &in.ToggleOverridesStatus, &out.ToggleOverridesStatus 204 | *out = make(map[string]bool, len(*in)) 205 | for key, val := range *in { 206 | (*out)[key] = val 207 | } 208 | } 209 | if in.StartupConfigResourceVersion != nil { 210 | in, out := &in.StartupConfigResourceVersion, &out.StartupConfigResourceVersion 211 | *out = new(string) 212 | **out = **in 213 | } 214 | } 215 | 216 | // DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new ConfigMapStatus. 217 | func (in *ConfigMapStatus) DeepCopy() *ConfigMapStatus { 218 | if in == nil { 219 | return nil 220 | } 221 | out := new(ConfigMapStatus) 222 | in.DeepCopyInto(out) 223 | return out 224 | } 225 | 226 | // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. 227 | func (in *PortConfig) DeepCopyInto(out *PortConfig) { 228 | *out = *in 229 | } 230 | 231 | // DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new PortConfig. 232 | func (in *PortConfig) DeepCopy() *PortConfig { 233 | if in == nil { 234 | return nil 235 | } 236 | out := new(PortConfig) 237 | in.DeepCopyInto(out) 238 | return out 239 | } 240 | 241 | // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. 242 | func (in *SelfSignedCertConfig) DeepCopyInto(out *SelfSignedCertConfig) { 243 | *out = *in 244 | } 245 | 246 | // DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new SelfSignedCertConfig. 247 | func (in *SelfSignedCertConfig) DeepCopy() *SelfSignedCertConfig { 248 | if in == nil { 249 | return nil 250 | } 251 | out := new(SelfSignedCertConfig) 252 | in.DeepCopyInto(out) 253 | return out 254 | } 255 | 256 | // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. 257 | func (in *ServiceConfig) DeepCopyInto(out *ServiceConfig) { 258 | *out = *in 259 | if in.TCPPorts != nil { 260 | in, out := &in.TCPPorts, &out.TCPPorts 261 | *out = make([]PortConfig, len(*in)) 262 | copy(*out, *in) 263 | } 264 | } 265 | 266 | // DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new ServiceConfig. 267 | func (in *ServiceConfig) DeepCopy() *ServiceConfig { 268 | if in == nil { 269 | return nil 270 | } 271 | out := new(ServiceConfig) 272 | in.DeepCopyInto(out) 273 | return out 274 | } 275 | -------------------------------------------------------------------------------- /config/crd/bases/ceoslab.arista.com_ceoslabdevices.yaml: -------------------------------------------------------------------------------- 1 | --- 2 | apiVersion: apiextensions.k8s.io/v1 3 | kind: CustomResourceDefinition 4 | metadata: 5 | annotations: 6 | controller-gen.kubebuilder.io/version: v0.8.0 7 | creationTimestamp: null 8 | name: ceoslabdevices.ceoslab.arista.com 9 | spec: 10 | group: ceoslab.arista.com 11 | names: 12 | kind: CEosLabDevice 13 | listKind: CEosLabDeviceList 14 | plural: ceoslabdevices 15 | singular: ceoslabdevice 16 | scope: Namespaced 17 | versions: 18 | - name: v1alpha1 19 | schema: 20 | openAPIV3Schema: 21 | description: CEosLabDevice is the Schema for the ceoslabdevices API 22 | properties: 23 | apiVersion: 24 | description: 'APIVersion defines the versioned schema of this representation 25 | of an object. Servers should convert recognized schemas to the latest 26 | internal value, and may reject unrecognized values. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#resources' 27 | type: string 28 | kind: 29 | description: 'Kind is a string value representing the REST resource this 30 | object represents. Servers may infer this from the endpoint the client 31 | submits requests to. Cannot be updated. In CamelCase. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds' 32 | type: string 33 | metadata: 34 | type: object 35 | spec: 36 | description: CEosLabDeviceSpec defines the desired state of CEosLabDevice 37 | properties: 38 | args: 39 | description: Additional arguments to pass to /sbin/init. Those necessary 40 | to boot properly are already present. 41 | items: 42 | type: string 43 | type: array 44 | certconfig: 45 | description: X.509 certificate configuration. 46 | properties: 47 | selfsignedcerts: 48 | description: Configuration for self-signed certificates. 49 | items: 50 | properties: 51 | certname: 52 | description: Certificate name on the node. 53 | type: string 54 | commonname: 55 | description: Common name to set in the cert. 56 | type: string 57 | keyname: 58 | description: Key name on the node. 59 | type: string 60 | keysize: 61 | description: RSA keysize to use for key generation. 62 | format: int32 63 | type: integer 64 | type: object 65 | type: array 66 | type: object 67 | envvars: 68 | additionalProperties: 69 | type: string 70 | description: Additional environment variables. Those necessary to 71 | boot properly are already present. 72 | type: object 73 | image: 74 | description: 'Image name. Default: ceos:latest' 75 | type: string 76 | initcontainerimage: 77 | description: 'Init container image name. Default: networkop/init-wait:latest' 78 | type: string 79 | intfmapping: 80 | additionalProperties: 81 | type: string 82 | description: Explicit interface mapping between kernel devices and 83 | interface names. If this is defined, any unmapped devices are ignored. 84 | type: object 85 | numinterfaces: 86 | description: 'Number of data interfaces to create. An additional interface 87 | (eth0) is created for pod connectivity. Default: 0 interfaces' 88 | format: int32 89 | type: integer 90 | resourcerequirements: 91 | additionalProperties: 92 | type: string 93 | description: 'Resource requests to configure on the pod. Default: 94 | none' 95 | type: object 96 | services: 97 | additionalProperties: 98 | properties: 99 | tcpports: 100 | description: TCP ports to forward to the pod. 101 | items: 102 | properties: 103 | in: 104 | description: Port inside the container. 105 | format: int32 106 | type: integer 107 | out: 108 | description: Port outside the container. Defaults to the 109 | same as in. 110 | format: int32 111 | type: integer 112 | type: object 113 | type: array 114 | type: object 115 | description: 'Port mappings for container services. Default: none' 116 | type: object 117 | sleep: 118 | description: 'Time (in seconds) to wait before starting the device. 119 | Default: 0 seconds' 120 | format: int32 121 | type: integer 122 | toggleoverrides: 123 | additionalProperties: 124 | type: boolean 125 | description: EOS feature toggle overrides 126 | type: object 127 | waitforagents: 128 | description: EOS agents to for the startup probe to block on 129 | items: 130 | type: string 131 | type: array 132 | type: object 133 | status: 134 | description: CEosLabDeviceStatus defines the observed state of CEosLabDevice 135 | properties: 136 | configmapconfig: 137 | description: ConfigMap state as configured in configmaps 138 | properties: 139 | intfmappingstatus: 140 | additionalProperties: 141 | type: string 142 | type: object 143 | rceosstale: 144 | type: boolean 145 | selfsignedcertstatus: 146 | additionalProperties: 147 | properties: 148 | certname: 149 | description: Certificate name on the node. 150 | type: string 151 | commonname: 152 | description: Common name to set in the cert. 153 | type: string 154 | keyname: 155 | description: Key name on the node. 156 | type: string 157 | keysize: 158 | description: RSA keysize to use for key generation. 159 | format: int32 160 | type: integer 161 | type: object 162 | type: object 163 | startupconfigresourceversion: 164 | type: string 165 | toggleoverridesstatus: 166 | additionalProperties: 167 | type: boolean 168 | type: object 169 | type: object 170 | podconfigmapconfig: 171 | description: ConfigMap state as present in the pod. If these diverge, 172 | we need to restart the pod to update. Even if an in-place update 173 | is possible these are needed at boot time. 174 | properties: 175 | intfmappingstatus: 176 | additionalProperties: 177 | type: string 178 | type: object 179 | rceosstale: 180 | type: boolean 181 | selfsignedcertstatus: 182 | additionalProperties: 183 | properties: 184 | certname: 185 | description: Certificate name on the node. 186 | type: string 187 | commonname: 188 | description: Common name to set in the cert. 189 | type: string 190 | keyname: 191 | description: Key name on the node. 192 | type: string 193 | keysize: 194 | description: RSA keysize to use for key generation. 195 | format: int32 196 | type: integer 197 | type: object 198 | type: object 199 | startupconfigresourceversion: 200 | type: string 201 | toggleoverridesstatus: 202 | additionalProperties: 203 | type: boolean 204 | type: object 205 | type: object 206 | reason: 207 | description: Reason for potential failure 208 | type: string 209 | status: 210 | description: Device status 211 | type: string 212 | type: object 213 | type: object 214 | served: true 215 | storage: true 216 | subresources: 217 | status: {} 218 | status: 219 | acceptedNames: 220 | kind: "" 221 | plural: "" 222 | conditions: [] 223 | storedVersions: [] 224 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | 2 | Apache License 3 | Version 2.0, January 2004 4 | http://www.apache.org/licenses/ 5 | 6 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 7 | 8 | 1. Definitions. 9 | 10 | "License" shall mean the terms and conditions for use, reproduction, 11 | and distribution as defined by Sections 1 through 9 of this document. 12 | 13 | "Licensor" shall mean the copyright owner or entity authorized by 14 | the copyright owner that is granting the License. 15 | 16 | "Legal Entity" shall mean the union of the acting entity and all 17 | other entities that control, are controlled by, or are under common 18 | control with that entity. For the purposes of this definition, 19 | "control" means (i) the power, direct or indirect, to cause the 20 | direction or management of such entity, whether by contract or 21 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 22 | outstanding shares, or (iii) beneficial ownership of such entity. 23 | 24 | "You" (or "Your") shall mean an individual or Legal Entity 25 | exercising permissions granted by this License. 26 | 27 | "Source" form shall mean the preferred form for making modifications, 28 | including but not limited to software source code, documentation 29 | source, and configuration files. 30 | 31 | "Object" form shall mean any form resulting from mechanical 32 | transformation or translation of a Source form, including but 33 | not limited to compiled object code, generated documentation, 34 | and conversions to other media types. 35 | 36 | "Work" shall mean the work of authorship, whether in Source or 37 | Object form, made available under the License, as indicated by a 38 | copyright notice that is included in or attached to the work 39 | (an example is provided in the Appendix below). 40 | 41 | "Derivative Works" shall mean any work, whether in Source or Object 42 | form, that is based on (or derived from) the Work and for which the 43 | editorial revisions, annotations, elaborations, or other modifications 44 | represent, as a whole, an original work of authorship. For the purposes 45 | of this License, Derivative Works shall not include works that remain 46 | separable from, or merely link (or bind by name) to the interfaces of, 47 | the Work and Derivative Works thereof. 48 | 49 | "Contribution" shall mean any work of authorship, including 50 | the original version of the Work and any modifications or additions 51 | to that Work or Derivative Works thereof, that is intentionally 52 | submitted to Licensor for inclusion in the Work by the copyright owner 53 | or by an individual or Legal Entity authorized to submit on behalf of 54 | the copyright owner. For the purposes of this definition, "submitted" 55 | means any form of electronic, verbal, or written communication sent 56 | to the Licensor or its representatives, including but not limited to 57 | communication on electronic mailing lists, source code control systems, 58 | and issue tracking systems that are managed by, or on behalf of, the 59 | Licensor for the purpose of discussing and improving the Work, but 60 | excluding communication that is conspicuously marked or otherwise 61 | designated in writing by the copyright owner as "Not a Contribution." 62 | 63 | "Contributor" shall mean Licensor and any individual or Legal Entity 64 | on behalf of whom a Contribution has been received by Licensor and 65 | subsequently incorporated within the Work. 66 | 67 | 2. Grant of Copyright License. Subject to the terms and conditions of 68 | this License, each Contributor hereby grants to You a perpetual, 69 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 70 | copyright license to reproduce, prepare Derivative Works of, 71 | publicly display, publicly perform, sublicense, and distribute the 72 | Work and such Derivative Works in Source or Object form. 73 | 74 | 3. Grant of Patent License. Subject to the terms and conditions of 75 | this License, each Contributor hereby grants to You a perpetual, 76 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 77 | (except as stated in this section) patent license to make, have made, 78 | use, offer to sell, sell, import, and otherwise transfer the Work, 79 | where such license applies only to those patent claims licensable 80 | by such Contributor that are necessarily infringed by their 81 | Contribution(s) alone or by combination of their Contribution(s) 82 | with the Work to which such Contribution(s) was submitted. If You 83 | institute patent litigation against any entity (including a 84 | cross-claim or counterclaim in a lawsuit) alleging that the Work 85 | or a Contribution incorporated within the Work constitutes direct 86 | or contributory patent infringement, then any patent licenses 87 | granted to You under this License for that Work shall terminate 88 | as of the date such litigation is filed. 89 | 90 | 4. Redistribution. You may reproduce and distribute copies of the 91 | Work or Derivative Works thereof in any medium, with or without 92 | modifications, and in Source or Object form, provided that You 93 | meet the following conditions: 94 | 95 | (a) You must give any other recipients of the Work or 96 | Derivative Works a copy of this License; and 97 | 98 | (b) You must cause any modified files to carry prominent notices 99 | stating that You changed the files; and 100 | 101 | (c) You must retain, in the Source form of any Derivative Works 102 | that You distribute, all copyright, patent, trademark, and 103 | attribution notices from the Source form of the Work, 104 | excluding those notices that do not pertain to any part of 105 | the Derivative Works; and 106 | 107 | (d) If the Work includes a "NOTICE" text file as part of its 108 | distribution, then any Derivative Works that You distribute must 109 | include a readable copy of the attribution notices contained 110 | within such NOTICE file, excluding those notices that do not 111 | pertain to any part of the Derivative Works, in at least one 112 | of the following places: within a NOTICE text file distributed 113 | as part of the Derivative Works; within the Source form or 114 | documentation, if provided along with the Derivative Works; or, 115 | within a display generated by the Derivative Works, if and 116 | wherever such third-party notices normally appear. The contents 117 | of the NOTICE file are for informational purposes only and 118 | do not modify the License. You may add Your own attribution 119 | notices within Derivative Works that You distribute, alongside 120 | or as an addendum to the NOTICE text from the Work, provided 121 | that such additional attribution notices cannot be construed 122 | as modifying the License. 123 | 124 | You may add Your own copyright statement to Your modifications and 125 | may provide additional or different license terms and conditions 126 | for use, reproduction, or distribution of Your modifications, or 127 | for any such Derivative Works as a whole, provided Your use, 128 | reproduction, and distribution of the Work otherwise complies with 129 | the conditions stated in this License. 130 | 131 | 5. Submission of Contributions. Unless You explicitly state otherwise, 132 | any Contribution intentionally submitted for inclusion in the Work 133 | by You to the Licensor shall be under the terms and conditions of 134 | this License, without any additional terms or conditions. 135 | Notwithstanding the above, nothing herein shall supersede or modify 136 | the terms of any separate license agreement you may have executed 137 | with Licensor regarding such Contributions. 138 | 139 | 6. Trademarks. This License does not grant permission to use the trade 140 | names, trademarks, service marks, or product names of the Licensor, 141 | except as required for reasonable and customary use in describing the 142 | origin of the Work and reproducing the content of the NOTICE file. 143 | 144 | 7. Disclaimer of Warranty. Unless required by applicable law or 145 | agreed to in writing, Licensor provides the Work (and each 146 | Contributor provides its Contributions) on an "AS IS" BASIS, 147 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 148 | implied, including, without limitation, any warranties or conditions 149 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 150 | PARTICULAR PURPOSE. You are solely responsible for determining the 151 | appropriateness of using or redistributing the Work and assume any 152 | risks associated with Your exercise of permissions under this License. 153 | 154 | 8. Limitation of Liability. In no event and under no legal theory, 155 | whether in tort (including negligence), contract, or otherwise, 156 | unless required by applicable law (such as deliberate and grossly 157 | negligent acts) or agreed to in writing, shall any Contributor be 158 | liable to You for damages, including any direct, indirect, special, 159 | incidental, or consequential damages of any character arising as a 160 | result of this License or out of the use or inability to use the 161 | Work (including but not limited to damages for loss of goodwill, 162 | work stoppage, computer failure or malfunction, or any and all 163 | other commercial damages or losses), even if such Contributor 164 | has been advised of the possibility of such damages. 165 | 166 | 9. Accepting Warranty or Additional Liability. While redistributing 167 | the Work or Derivative Works thereof, You may choose to offer, 168 | and charge a fee for, acceptance of support, warranty, indemnity, 169 | or other liability obligations and/or rights consistent with this 170 | License. However, in accepting such obligations, You may act only 171 | on Your own behalf and on Your sole responsibility, not on behalf 172 | of any other Contributor, and only if You agree to indemnify, 173 | defend, and hold each Contributor harmless for any liability 174 | incurred by, or claims asserted against, such Contributor by reason 175 | of your accepting any such warranty or additional liability. 176 | 177 | END OF TERMS AND CONDITIONS 178 | -------------------------------------------------------------------------------- /config/kustomized/manifest.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: v1 2 | kind: Namespace 3 | metadata: 4 | labels: 5 | control-plane: controller-manager 6 | name: arista-ceoslab-operator-system 7 | --- 8 | apiVersion: apiextensions.k8s.io/v1 9 | kind: CustomResourceDefinition 10 | metadata: 11 | annotations: 12 | controller-gen.kubebuilder.io/version: v0.8.0 13 | creationTimestamp: null 14 | name: ceoslabdevices.ceoslab.arista.com 15 | spec: 16 | group: ceoslab.arista.com 17 | names: 18 | kind: CEosLabDevice 19 | listKind: CEosLabDeviceList 20 | plural: ceoslabdevices 21 | singular: ceoslabdevice 22 | scope: Namespaced 23 | versions: 24 | - name: v1alpha1 25 | schema: 26 | openAPIV3Schema: 27 | description: CEosLabDevice is the Schema for the ceoslabdevices API 28 | properties: 29 | apiVersion: 30 | description: 'APIVersion defines the versioned schema of this representation of an object. Servers should convert recognized schemas to the latest internal value, and may reject unrecognized values. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#resources' 31 | type: string 32 | kind: 33 | description: 'Kind is a string value representing the REST resource this object represents. Servers may infer this from the endpoint the client submits requests to. Cannot be updated. In CamelCase. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds' 34 | type: string 35 | metadata: 36 | type: object 37 | spec: 38 | description: CEosLabDeviceSpec defines the desired state of CEosLabDevice 39 | properties: 40 | args: 41 | description: Additional arguments to pass to /sbin/init. Those necessary to boot properly are already present. 42 | items: 43 | type: string 44 | type: array 45 | certconfig: 46 | description: X.509 certificate configuration. 47 | properties: 48 | selfsignedcerts: 49 | description: Configuration for self-signed certificates. 50 | items: 51 | properties: 52 | certname: 53 | description: Certificate name on the node. 54 | type: string 55 | commonname: 56 | description: Common name to set in the cert. 57 | type: string 58 | keyname: 59 | description: Key name on the node. 60 | type: string 61 | keysize: 62 | description: RSA keysize to use for key generation. 63 | format: int32 64 | type: integer 65 | type: object 66 | type: array 67 | type: object 68 | envvars: 69 | additionalProperties: 70 | type: string 71 | description: Additional environment variables. Those necessary to boot properly are already present. 72 | type: object 73 | image: 74 | description: 'Image name. Default: ceos:latest' 75 | type: string 76 | initcontainerimage: 77 | description: 'Init container image name. Default: networkop/init-wait:latest' 78 | type: string 79 | intfmapping: 80 | additionalProperties: 81 | type: string 82 | description: Explicit interface mapping between kernel devices and interface names. If this is defined, any unmapped devices are ignored. 83 | type: object 84 | numinterfaces: 85 | description: 'Number of data interfaces to create. An additional interface (eth0) is created for pod connectivity. Default: 0 interfaces' 86 | format: int32 87 | type: integer 88 | resourcerequirements: 89 | additionalProperties: 90 | type: string 91 | description: 'Resource requests to configure on the pod. Default: none' 92 | type: object 93 | services: 94 | additionalProperties: 95 | properties: 96 | tcpports: 97 | description: TCP ports to forward to the pod. 98 | items: 99 | properties: 100 | in: 101 | description: Port inside the container. 102 | format: int32 103 | type: integer 104 | out: 105 | description: Port outside the container. Defaults to the same as in. 106 | format: int32 107 | type: integer 108 | type: object 109 | type: array 110 | type: object 111 | description: 'Port mappings for container services. Default: none' 112 | type: object 113 | sleep: 114 | description: 'Time (in seconds) to wait before starting the device. Default: 0 seconds' 115 | format: int32 116 | type: integer 117 | toggleoverrides: 118 | additionalProperties: 119 | type: boolean 120 | description: EOS feature toggle overrides 121 | type: object 122 | waitforagents: 123 | description: EOS agents to for the startup probe to block on 124 | items: 125 | type: string 126 | type: array 127 | type: object 128 | status: 129 | description: CEosLabDeviceStatus defines the observed state of CEosLabDevice 130 | properties: 131 | configmapconfig: 132 | description: ConfigMap state as configured in configmaps 133 | properties: 134 | intfmappingstatus: 135 | additionalProperties: 136 | type: string 137 | type: object 138 | rceosstale: 139 | type: boolean 140 | selfsignedcertstatus: 141 | additionalProperties: 142 | properties: 143 | certname: 144 | description: Certificate name on the node. 145 | type: string 146 | commonname: 147 | description: Common name to set in the cert. 148 | type: string 149 | keyname: 150 | description: Key name on the node. 151 | type: string 152 | keysize: 153 | description: RSA keysize to use for key generation. 154 | format: int32 155 | type: integer 156 | type: object 157 | type: object 158 | startupconfigresourceversion: 159 | type: string 160 | toggleoverridesstatus: 161 | additionalProperties: 162 | type: boolean 163 | type: object 164 | type: object 165 | podconfigmapconfig: 166 | description: ConfigMap state as present in the pod. If these diverge, we need to restart the pod to update. Even if an in-place update is possible these are needed at boot time. 167 | properties: 168 | intfmappingstatus: 169 | additionalProperties: 170 | type: string 171 | type: object 172 | rceosstale: 173 | type: boolean 174 | selfsignedcertstatus: 175 | additionalProperties: 176 | properties: 177 | certname: 178 | description: Certificate name on the node. 179 | type: string 180 | commonname: 181 | description: Common name to set in the cert. 182 | type: string 183 | keyname: 184 | description: Key name on the node. 185 | type: string 186 | keysize: 187 | description: RSA keysize to use for key generation. 188 | format: int32 189 | type: integer 190 | type: object 191 | type: object 192 | startupconfigresourceversion: 193 | type: string 194 | toggleoverridesstatus: 195 | additionalProperties: 196 | type: boolean 197 | type: object 198 | type: object 199 | reason: 200 | description: Reason for potential failure 201 | type: string 202 | status: 203 | description: Device status 204 | type: string 205 | type: object 206 | type: object 207 | served: true 208 | storage: true 209 | subresources: 210 | status: {} 211 | status: 212 | acceptedNames: 213 | kind: "" 214 | plural: "" 215 | conditions: [] 216 | storedVersions: [] 217 | --- 218 | apiVersion: v1 219 | kind: ServiceAccount 220 | metadata: 221 | name: arista-ceoslab-operator-controller-manager 222 | namespace: arista-ceoslab-operator-system 223 | --- 224 | apiVersion: rbac.authorization.k8s.io/v1 225 | kind: Role 226 | metadata: 227 | name: arista-ceoslab-operator-leader-election-role 228 | namespace: arista-ceoslab-operator-system 229 | rules: 230 | - apiGroups: 231 | - "" 232 | resources: 233 | - configmaps 234 | verbs: 235 | - get 236 | - list 237 | - watch 238 | - create 239 | - update 240 | - patch 241 | - delete 242 | - apiGroups: 243 | - coordination.k8s.io 244 | resources: 245 | - leases 246 | verbs: 247 | - get 248 | - list 249 | - watch 250 | - create 251 | - update 252 | - patch 253 | - delete 254 | - apiGroups: 255 | - "" 256 | resources: 257 | - events 258 | verbs: 259 | - create 260 | - patch 261 | --- 262 | apiVersion: rbac.authorization.k8s.io/v1 263 | kind: ClusterRole 264 | metadata: 265 | creationTimestamp: null 266 | name: arista-ceoslab-operator-manager-role 267 | rules: 268 | - apiGroups: 269 | - ceoslab.arista.com 270 | resources: 271 | - ceoslabdevices 272 | verbs: 273 | - create 274 | - delete 275 | - get 276 | - list 277 | - patch 278 | - update 279 | - watch 280 | - apiGroups: 281 | - ceoslab.arista.com 282 | resources: 283 | - ceoslabdevices/finalizers 284 | verbs: 285 | - update 286 | - apiGroups: 287 | - ceoslab.arista.com 288 | resources: 289 | - ceoslabdevices/status 290 | verbs: 291 | - get 292 | - patch 293 | - update 294 | - apiGroups: 295 | - "" 296 | resources: 297 | - configmaps 298 | verbs: 299 | - create 300 | - delete 301 | - get 302 | - list 303 | - patch 304 | - update 305 | - watch 306 | - apiGroups: 307 | - "" 308 | resources: 309 | - pods 310 | verbs: 311 | - create 312 | - delete 313 | - get 314 | - list 315 | - patch 316 | - update 317 | - watch 318 | - apiGroups: 319 | - "" 320 | resources: 321 | - secrets 322 | verbs: 323 | - create 324 | - delete 325 | - get 326 | - list 327 | - patch 328 | - update 329 | - watch 330 | - apiGroups: 331 | - "" 332 | resources: 333 | - services 334 | verbs: 335 | - create 336 | - delete 337 | - get 338 | - list 339 | - patch 340 | - update 341 | - watch 342 | --- 343 | apiVersion: rbac.authorization.k8s.io/v1 344 | kind: ClusterRole 345 | metadata: 346 | name: arista-ceoslab-operator-metrics-reader 347 | rules: 348 | - nonResourceURLs: 349 | - /metrics 350 | verbs: 351 | - get 352 | --- 353 | apiVersion: rbac.authorization.k8s.io/v1 354 | kind: ClusterRole 355 | metadata: 356 | name: arista-ceoslab-operator-proxy-role 357 | rules: 358 | - apiGroups: 359 | - authentication.k8s.io 360 | resources: 361 | - tokenreviews 362 | verbs: 363 | - create 364 | - apiGroups: 365 | - authorization.k8s.io 366 | resources: 367 | - subjectaccessreviews 368 | verbs: 369 | - create 370 | --- 371 | apiVersion: rbac.authorization.k8s.io/v1 372 | kind: RoleBinding 373 | metadata: 374 | name: arista-ceoslab-operator-leader-election-rolebinding 375 | namespace: arista-ceoslab-operator-system 376 | roleRef: 377 | apiGroup: rbac.authorization.k8s.io 378 | kind: Role 379 | name: arista-ceoslab-operator-leader-election-role 380 | subjects: 381 | - kind: ServiceAccount 382 | name: arista-ceoslab-operator-controller-manager 383 | namespace: arista-ceoslab-operator-system 384 | --- 385 | apiVersion: rbac.authorization.k8s.io/v1 386 | kind: ClusterRoleBinding 387 | metadata: 388 | name: arista-ceoslab-operator-manager-rolebinding 389 | roleRef: 390 | apiGroup: rbac.authorization.k8s.io 391 | kind: ClusterRole 392 | name: arista-ceoslab-operator-manager-role 393 | subjects: 394 | - kind: ServiceAccount 395 | name: arista-ceoslab-operator-controller-manager 396 | namespace: arista-ceoslab-operator-system 397 | --- 398 | apiVersion: rbac.authorization.k8s.io/v1 399 | kind: ClusterRoleBinding 400 | metadata: 401 | name: arista-ceoslab-operator-proxy-rolebinding 402 | roleRef: 403 | apiGroup: rbac.authorization.k8s.io 404 | kind: ClusterRole 405 | name: arista-ceoslab-operator-proxy-role 406 | subjects: 407 | - kind: ServiceAccount 408 | name: arista-ceoslab-operator-controller-manager 409 | namespace: arista-ceoslab-operator-system 410 | --- 411 | apiVersion: v1 412 | data: 413 | controller_manager_config.yaml: | 414 | apiVersion: controller-runtime.sigs.k8s.io/v1alpha1 415 | kind: ControllerManagerConfig 416 | health: 417 | healthProbeBindAddress: :8081 418 | metrics: 419 | bindAddress: 127.0.0.1:8080 420 | webhook: 421 | port: 9443 422 | leaderElection: 423 | leaderElect: true 424 | resourceName: a8645112.arista.com 425 | kind: ConfigMap 426 | metadata: 427 | name: arista-ceoslab-operator-manager-config 428 | namespace: arista-ceoslab-operator-system 429 | --- 430 | apiVersion: v1 431 | kind: Service 432 | metadata: 433 | labels: 434 | control-plane: controller-manager 435 | name: arista-ceoslab-operator-controller-manager-metrics-service 436 | namespace: arista-ceoslab-operator-system 437 | spec: 438 | ports: 439 | - name: https 440 | port: 8443 441 | protocol: TCP 442 | targetPort: https 443 | selector: 444 | control-plane: controller-manager 445 | --- 446 | apiVersion: apps/v1 447 | kind: Deployment 448 | metadata: 449 | labels: 450 | control-plane: controller-manager 451 | name: arista-ceoslab-operator-controller-manager 452 | namespace: arista-ceoslab-operator-system 453 | spec: 454 | replicas: 1 455 | selector: 456 | matchLabels: 457 | control-plane: controller-manager 458 | template: 459 | metadata: 460 | annotations: 461 | kubectl.kubernetes.io/default-container: manager 462 | labels: 463 | control-plane: controller-manager 464 | spec: 465 | containers: 466 | - args: 467 | - --secure-listen-address=0.0.0.0:8443 468 | - --upstream=http://127.0.0.1:8080/ 469 | - --logtostderr=true 470 | - --v=0 471 | image: gcr.io/kubebuilder/kube-rbac-proxy:v0.11.0 472 | name: kube-rbac-proxy 473 | ports: 474 | - containerPort: 8443 475 | name: https 476 | protocol: TCP 477 | resources: 478 | limits: 479 | cpu: 500m 480 | memory: 128Mi 481 | requests: 482 | cpu: 5m 483 | memory: 64Mi 484 | - args: 485 | - --health-probe-bind-address=:8081 486 | - --metrics-bind-address=127.0.0.1:8080 487 | - --leader-elect 488 | command: 489 | - /manager 490 | image: ghcr.io/aristanetworks/arista-ceoslab-operator:v2.1.2 491 | livenessProbe: 492 | httpGet: 493 | path: /healthz 494 | port: 8081 495 | initialDelaySeconds: 15 496 | periodSeconds: 20 497 | name: manager 498 | readinessProbe: 499 | httpGet: 500 | path: /readyz 501 | port: 8081 502 | initialDelaySeconds: 5 503 | periodSeconds: 10 504 | resources: 505 | limits: 506 | cpu: 500m 507 | memory: 128Mi 508 | requests: 509 | cpu: 10m 510 | memory: 64Mi 511 | securityContext: 512 | allowPrivilegeEscalation: false 513 | securityContext: 514 | runAsNonRoot: true 515 | serviceAccountName: arista-ceoslab-operator-controller-manager 516 | terminationGracePeriodSeconds: 10 517 | -------------------------------------------------------------------------------- /controllers/ceoslabdevice_controller.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2022. 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | 17 | package controllers 18 | 19 | import ( 20 | "bytes" 21 | "context" 22 | "crypto/rand" 23 | "crypto/rsa" 24 | "crypto/x509" 25 | "crypto/x509/pkix" 26 | "encoding/json" 27 | "encoding/pem" 28 | "fmt" 29 | "math/big" 30 | "reflect" 31 | "regexp" 32 | "runtime" 33 | "sort" 34 | "strconv" 35 | "strings" 36 | "time" 37 | 38 | ceoslabv1alpha1 "github.com/aristanetworks/arista-ceoslab-operator/v2/api/v1alpha1" 39 | 40 | corev1 "k8s.io/api/core/v1" 41 | "k8s.io/apimachinery/pkg/api/errors" 42 | "k8s.io/apimachinery/pkg/api/resource" 43 | apiRuntime "k8s.io/apimachinery/pkg/runtime" 44 | "k8s.io/apimachinery/pkg/types" 45 | "k8s.io/apimachinery/pkg/util/intstr" 46 | "k8s.io/utils/pointer" 47 | 48 | ctrl "sigs.k8s.io/controller-runtime" 49 | "sigs.k8s.io/controller-runtime/pkg/client" 50 | rctrl "sigs.k8s.io/controller-runtime/pkg/controller" 51 | kLog "sigs.k8s.io/controller-runtime/pkg/log" 52 | ) 53 | 54 | const ( 55 | FAILED_STATE = "failed" 56 | INPROGRESS_STATE = "reconciling" 57 | SUCCESS_STATE = "success" 58 | 59 | DEFAULT_INIT_CONTAINER_IMAGE = "networkop/init-wait:latest" 60 | CEOS_COMMAND = "/sbin/init" 61 | DEFAULT_CEOS_IMAGE = "ceos:latest" 62 | ) 63 | 64 | var ( 65 | defaultEnvVars = map[string]string{ 66 | "CEOS": "1", 67 | "EOS_PLATFORM": "ceoslab", 68 | "container": "docker", 69 | "ETBA": "1", 70 | "SKIP_ZEROTOUCH_BARRIER_IN_SYSDBINIT": "1", 71 | "INTFTYPE": "eth", 72 | } 73 | 74 | requeue = ctrl.Result{Requeue: true} 75 | noRequeue = ctrl.Result{} 76 | ethIntfRe = regexp.MustCompile(`^Ethernet\d+(?:/\d+)?(?:/\d+)?$`) 77 | mgmtIntfRe = regexp.MustCompile(`^Management\d+(?:/\d+)?$`) 78 | ) 79 | 80 | // CEosLabDeviceReconciler reconciles a CEosLabDevice object 81 | type CEosLabDeviceReconciler struct { 82 | client.Client 83 | Scheme *apiRuntime.Scheme 84 | } 85 | 86 | // Helpers for updating status 87 | func (r *CEosLabDeviceReconciler) updateDeviceFail(ctx context.Context, device *ceoslabv1alpha1.CEosLabDevice, errMsg string) { 88 | device.Status.State = FAILED_STATE 89 | device.Status.Reason = errMsg 90 | r.Status().Update(ctx, device) 91 | r.Update(ctx, device) 92 | } 93 | 94 | func (r *CEosLabDeviceReconciler) updateDeviceReconciling(ctx context.Context, device *ceoslabv1alpha1.CEosLabDevice, msg string) { 95 | device.Status.State = INPROGRESS_STATE 96 | device.Status.Reason = msg 97 | r.Status().Update(ctx, device) 98 | r.Update(ctx, device) 99 | } 100 | 101 | func (r *CEosLabDeviceReconciler) updateDeviceSuccess(ctx context.Context, device *ceoslabv1alpha1.CEosLabDevice) { 102 | device.Status.State = SUCCESS_STATE 103 | device.Status.Reason = "" 104 | r.Status().Update(ctx, device) 105 | r.Update(ctx, device) 106 | } 107 | 108 | //+kubebuilder:rbac:groups=ceoslab.arista.com,resources=ceoslabdevices,verbs=get;list;watch;create;update;patch;delete 109 | //+kubebuilder:rbac:groups=ceoslab.arista.com,resources=ceoslabdevices/status,verbs=get;update;patch 110 | //+kubebuilder:rbac:groups=ceoslab.arista.com,resources=ceoslabdevices/finalizers,verbs=update 111 | 112 | //+kubebuilder:rbac:groups=core,resources=pods,verbs=get;list;watch;create;update;patch;delete 113 | //+kubebuilder:rbac:groups=core,resources=services,verbs=get;list;watch;create;update;patch;delete 114 | //+kubebuilder:rbac:groups=core,resources=configmaps,verbs=get;list;watch;create;update;patch;delete 115 | //+kubebuilder:rbac:groups=core,resources=secrets,verbs=get;list;watch;create;update;patch;delete 116 | 117 | // Reconcile is part of the main kubernetes reconciliation loop which aims to 118 | // move the current state of the cluster closer to the desired state. 119 | // 120 | // For more details, check Reconcile and its Result here: 121 | // - https://pkg.go.dev/sigs.k8s.io/controller-runtime@v0.11.2/pkg/reconcile 122 | func (r *CEosLabDeviceReconciler) Reconcile(ctx context.Context, req ctrl.Request) (ctrl.Result, error) { 123 | log := kLog.FromContext(ctx) 124 | 125 | // Fetch the CEosLabDevice instance 126 | device := &ceoslabv1alpha1.CEosLabDevice{} 127 | err := r.Get(ctx, req.NamespacedName, device) 128 | if err != nil { 129 | if errors.IsNotFound(err) { 130 | // Request object not found, could have been deleted after reconcile request. 131 | // Owned objects are automatically garbage collected. For additional cleanup logic use finalizers. 132 | // Return and don't requeue 133 | log.Info("CEosLabDevice resource not found. Ignoring since object must be deleted") 134 | return noRequeue, nil 135 | } 136 | // Error reading the object - requeue the request. 137 | log.Error(err, "Failed to get CEosLabDevice") 138 | return requeue, err 139 | } 140 | 141 | // Create or retrieve k8s objects associated with this CEosLabDevice and validate their 142 | // state. We create configmaps to mount a few files into the container, the pod itself, 143 | // and possibly a set of services (if configured in the spec). 144 | 145 | // The error handling for creating and pushing k8s resources is very repetitive so it's 146 | // been factored out. 147 | 148 | // ConfigMaps and Secrets 149 | 150 | // Store certificate config to simplify reconciliation later. Keyed by secret name. 151 | selfSignedCertConfigs := map[string]ceoslabv1alpha1.SelfSignedCertConfig{} 152 | 153 | // Also, collect them into a map so we can generate volumes from them. 154 | secretsAndConfigMaps := map[string]client.Object{} 155 | 156 | configMapStatus := &device.Status.ConfigMapStatus 157 | 158 | // Certificates 159 | selfSignedCerts := device.Spec.CertConfig.SelfSignedCerts 160 | selfSignedCertsConfed := len(selfSignedCerts) > 0 161 | for i, certConfig := range selfSignedCerts { 162 | name := fmt.Sprintf("secret-selfsigned-%s-%d", device.Name, i) 163 | createObject := func(object client.Object) error { 164 | err := getSelfSignedCert(object.(*corev1.Secret), certConfig, device.Name) 165 | if err != nil { 166 | return err 167 | } 168 | ssCertStatus := &configMapStatus.SelfSignedCertStatus 169 | if *ssCertStatus == nil { 170 | *ssCertStatus = map[string]ceoslabv1alpha1.SelfSignedCertConfig{} 171 | } 172 | (*ssCertStatus)[name] = certConfig 173 | // Need to generate new rc.eos to reflect new certificates 174 | configMapStatus.RcEosStale = true 175 | return nil 176 | } 177 | secret := &corev1.Secret{} 178 | err := r.getOrCreateObject(ctx, device, name, createObject, secret) 179 | if err != nil { 180 | return noRequeue, err 181 | } 182 | secretsAndConfigMaps[name] = secret 183 | selfSignedCertConfigs[name] = certConfig 184 | } 185 | 186 | // Generate rc.eos to load certificates from /mnt/flash into in-memory filesystem 187 | rcEosName := fmt.Sprintf("configmap-rceos-%s", device.Name) 188 | if selfSignedCertsConfed { 189 | createObject := func(object client.Object) error { 190 | getRcEos(object.(*corev1.ConfigMap), selfSignedCerts) 191 | configMapStatus.RcEosStale = false 192 | return nil 193 | } 194 | configMap := &corev1.ConfigMap{} 195 | err := r.getOrCreateObject(ctx, device, rcEosName, createObject, configMap) 196 | if err != nil { 197 | return noRequeue, err 198 | } 199 | secretsAndConfigMaps[rcEosName] = configMap 200 | } 201 | 202 | // Explicit kernel device to EOS interface mapping 203 | intfMapping := device.Spec.IntfMapping 204 | intfMappingName := fmt.Sprintf("configmap-intfmapping-%s", device.Name) 205 | intfMappingConfed := len(intfMapping) > 0 206 | if intfMappingConfed { 207 | createObject := func(object client.Object) error { 208 | err := getJsonIntfMapping(object.(*corev1.ConfigMap), intfMapping) 209 | if err != nil { 210 | return err 211 | } 212 | configMapStatus.IntfMappingStatus = intfMapping 213 | return nil 214 | } 215 | configMap := &corev1.ConfigMap{} 216 | err := r.getOrCreateObject(ctx, device, intfMappingName, createObject, configMap) 217 | if err != nil { 218 | return noRequeue, err 219 | } 220 | secretsAndConfigMaps[intfMappingName] = configMap 221 | } 222 | 223 | // Feature toggle overrides 224 | toggleOverrides := device.Spec.ToggleOverrides 225 | toggleOverridesName := fmt.Sprintf("configmap-toggle-override-%s", device.Name) 226 | toggleOverridesConfed := len(toggleOverrides) > 0 227 | if toggleOverridesConfed { 228 | createObject := func(object client.Object) error { 229 | getToggleOverrides(object.(*corev1.ConfigMap), toggleOverrides) 230 | configMapStatus.ToggleOverridesStatus = toggleOverrides 231 | return nil 232 | } 233 | configMap := &corev1.ConfigMap{} 234 | err := r.getOrCreateObject(ctx, device, toggleOverridesName, createObject, configMap) 235 | if err != nil { 236 | return noRequeue, err 237 | } 238 | secretsAndConfigMaps[toggleOverridesName] = configMap 239 | } 240 | 241 | // KNE may create a configmap to be used as a startup config. 242 | var startupConfigResourceVersion *string = nil 243 | startupConfigName := fmt.Sprintf("%s-config", device.Name) 244 | { 245 | configMap := &corev1.ConfigMap{} 246 | err := r.Get(ctx, types.NamespacedName{Name: startupConfigName, Namespace: device.Namespace}, configMap) 247 | if err != nil && !errors.IsNotFound(err) { 248 | errMsg := fmt.Sprintf("Failed to get %s for CEosLabDevice %s", startupConfigName, device.Name) 249 | log.Error(err, errMsg) 250 | r.updateDeviceFail(ctx, device, errMsg) 251 | return noRequeue, err 252 | } else if err == nil { 253 | // KNE created the configmap 254 | startupConfigResourceVersion = pointer.String(configMap.ObjectMeta.ResourceVersion) 255 | configMapStatus.StartupConfigResourceVersion = startupConfigResourceVersion 256 | secretsAndConfigMaps[startupConfigName] = configMap 257 | } 258 | } 259 | 260 | // Pods 261 | 262 | devicePod := &corev1.Pod{} 263 | { 264 | podName := device.Name 265 | createPodObject := func(object client.Object) error { 266 | err := getPod(object.(*corev1.Pod), device, secretsAndConfigMaps) 267 | if err != nil { 268 | return err 269 | } 270 | // Write back the configmap state at the time the pod is created. This way 271 | // we can resolve discrepencies between the pod and configmaps even if 272 | // the configmaps have already been updated. 273 | (&device.Status.ConfigMapStatus).DeepCopyInto(&device.Status.PodConfigMapStatus) 274 | return nil 275 | } 276 | err := r.getOrCreateObject(ctx, device, podName, createPodObject, devicePod) 277 | if err != nil { 278 | return noRequeue, err 279 | } 280 | } 281 | 282 | // Services 283 | 284 | deviceService := &corev1.Service{} 285 | serviceName := fmt.Sprintf("service-%s", device.Name) 286 | servicesConfed := len(device.Spec.Services) > 0 287 | if servicesConfed { 288 | getServiceObject := func(object client.Object) error { 289 | err := getService(object.(*corev1.Service), device) 290 | if err != nil { 291 | return err 292 | } 293 | return nil 294 | } 295 | err := r.getOrCreateObject(ctx, device, serviceName, getServiceObject, deviceService) 296 | if err != nil { 297 | return noRequeue, err 298 | } 299 | } 300 | 301 | // Reconcile observed state against desired state 302 | 303 | doDeleteConfigMap := func(name string, object client.Object) error { 304 | msg := fmt.Sprintf("Deleting %s for CEosLabDevice %s", name, device.Name) 305 | log.Info(msg) 306 | err := r.Get(ctx, types.NamespacedName{Name: name, Namespace: device.Namespace}, object) 307 | if err == nil { 308 | r.Delete(ctx, object) 309 | } else if err != nil && !errors.IsNotFound(err) { 310 | errMsg := fmt.Sprintf("Could not delete %s for CEosLabDevice %s", name, device.Name) 311 | log.Error(err, errMsg) 312 | r.updateDeviceFail(ctx, device, errMsg) 313 | return err 314 | } 315 | return nil 316 | } 317 | 318 | // Certificates 319 | for name, certConfig := range selfSignedCertConfigs { 320 | certStatus := configMapStatus.SelfSignedCertStatus[name] 321 | if certConfig != certStatus { 322 | msg := fmt.Sprintf("Cert %s out-of-sync for CEosLabDevice %s", name, device.Name) 323 | log.Info(msg) 324 | configMapStatus.RcEosStale = true 325 | doDeleteConfigMap(name, &corev1.Secret{}) 326 | r.updateDeviceReconciling(ctx, device, msg) 327 | return requeue, nil 328 | } 329 | } 330 | for name := range configMapStatus.SelfSignedCertStatus { 331 | if _, present := selfSignedCertConfigs[name]; !present { 332 | msg := fmt.Sprintf("Cert %s deleted from CEosLabDevice %s", name, device.Name) 333 | log.Info(msg) 334 | configMapStatus.RcEosStale = true 335 | delete(configMapStatus.SelfSignedCertStatus, name) 336 | if len(configMapStatus.SelfSignedCertStatus) == 0 { 337 | configMapStatus.SelfSignedCertStatus = nil 338 | } 339 | err := doDeleteConfigMap(name, &corev1.Secret{}) 340 | if err != nil { 341 | return noRequeue, err 342 | } 343 | r.updateDeviceReconciling(ctx, device, msg) 344 | return requeue, nil 345 | } 346 | } 347 | 348 | // rc.eos boot script 349 | if configMapStatus.RcEosStale { 350 | msg := "" 351 | if selfSignedCertsConfed { 352 | msg = fmt.Sprintf("rc.eos out-of-sync for CEosLabDevice %s", device.Name) 353 | log.Info(msg) 354 | } else { 355 | msg = fmt.Sprintf("Deleting rc.eos for CEosLabDevice %s", device.Name) 356 | log.Info(msg) 357 | configMapStatus.RcEosStale = false 358 | } 359 | doDeleteConfigMap(rcEosName, &corev1.ConfigMap{}) 360 | r.updateDeviceReconciling(ctx, device, msg) 361 | return requeue, nil 362 | } 363 | 364 | // Explicit kernel device to EOS interface mapping 365 | intfMappingStatus := configMapStatus.IntfMappingStatus 366 | if intfMappingStatus != nil { 367 | if intfMappingConfed { 368 | if !reflect.DeepEqual(intfMapping, intfMappingStatus) { 369 | msg := fmt.Sprintf("Interface mapping out-of-sync for CEosLabDevice %s", 370 | device.Name) 371 | log.Info(msg) 372 | doDeleteConfigMap(intfMappingName, &corev1.ConfigMap{}) 373 | r.updateDeviceReconciling(ctx, device, msg) 374 | return requeue, nil 375 | } 376 | } else { 377 | msg := fmt.Sprintf("Interface mapping deleted from CEosLabDevice %s", 378 | device.Name) 379 | log.Info(msg) 380 | configMapStatus.IntfMappingStatus = nil 381 | doDeleteConfigMap(intfMappingName, &corev1.ConfigMap{}) 382 | if err != nil { 383 | return noRequeue, err 384 | } 385 | r.updateDeviceReconciling(ctx, device, msg) 386 | return requeue, nil 387 | } 388 | } 389 | 390 | // EOS feature toggle overrides 391 | toggleOverridesStatus := configMapStatus.ToggleOverridesStatus 392 | if toggleOverridesStatus != nil { 393 | if toggleOverridesConfed { 394 | if !reflect.DeepEqual(toggleOverrides, toggleOverridesStatus) { 395 | msg := fmt.Sprintf("Toggle overrides out-of-sync for CEosLabDevice %s", 396 | device.Name) 397 | log.Info(msg) 398 | doDeleteConfigMap(toggleOverridesName, &corev1.ConfigMap{}) 399 | r.updateDeviceReconciling(ctx, device, msg) 400 | return requeue, nil 401 | } 402 | } else { 403 | msg := fmt.Sprintf("Toggle overrides deleted from CEosLabDevice %s", 404 | device.Name) 405 | log.Info(msg) 406 | configMapStatus.ToggleOverridesStatus = nil 407 | doDeleteConfigMap(toggleOverridesName, &corev1.ConfigMap{}) 408 | if err != nil { 409 | return noRequeue, err 410 | } 411 | r.updateDeviceReconciling(ctx, device, msg) 412 | return requeue, nil 413 | } 414 | } 415 | 416 | // Startup config 417 | startupConfigResourceVersionStatus := configMapStatus.StartupConfigResourceVersion 418 | if startupConfigResourceVersionStatus != nil { 419 | if startupConfigResourceVersion != nil { 420 | if *startupConfigResourceVersionStatus != *startupConfigResourceVersion { 421 | // No need to update the configmap, KNE already did. But we do 422 | // need to update the state which will trigger a pod restart 423 | // next reconciliation because the configmap and pod configmap 424 | // statuses will diverge. 425 | configMapStatus.StartupConfigResourceVersion = startupConfigResourceVersion 426 | msg := fmt.Sprintf("Startup-config out-of-sync for CEosLabDevice %s", device.Name) 427 | log.Info(msg) 428 | r.updateDeviceReconciling(ctx, device, msg) 429 | return requeue, nil 430 | } 431 | } else { 432 | // Again, no need to delete anything, but we still need to update the 433 | // status for the same reason. 434 | configMapStatus.StartupConfigResourceVersion = nil 435 | msg := fmt.Sprintf("Startup-config deleted from CEosLabDevice %s", device.Name) 436 | log.Info(msg) 437 | r.updateDeviceReconciling(ctx, device, msg) 438 | return requeue, nil 439 | } 440 | } 441 | 442 | // Ensure pod configmap config is the same as the current configmap config 443 | podConfigMapStatus := &device.Status.PodConfigMapStatus 444 | if !reflect.DeepEqual(podConfigMapStatus, configMapStatus) { 445 | msg := fmt.Sprintf("Updating CEosLabDevice %s configmaps, new: %v, old; %v", 446 | device.Name, configMapStatus, podConfigMapStatus) 447 | log.Info(msg) 448 | // These configmaps are required at boot time so we need to restart the pod 449 | // Delete the pod, and requeue to recreate 450 | r.Delete(ctx, devicePod) 451 | r.updateDeviceReconciling(ctx, device, msg) 452 | return requeue, nil 453 | } 454 | 455 | // Ensure the pod labels are the same as the metadata 456 | labels := getLabels(device) 457 | podLabels := devicePod.ObjectMeta.Labels 458 | if !reflect.DeepEqual(labels, podLabels) { 459 | msg := fmt.Sprintf("Updating CEosLabDevice %s pod labels, new: %v, old: %v", 460 | device.Name, labels, podLabels) 461 | log.Info(msg) 462 | // Update pod 463 | devicePod.ObjectMeta.Labels = labels 464 | r.Update(ctx, devicePod) 465 | // Update status 466 | r.updateDeviceReconciling(ctx, device, msg) 467 | return requeue, nil 468 | } 469 | 470 | // Ensure the pod environment variables are the same as the spec 471 | container := devicePod.Spec.Containers[0] 472 | envVarsMap := getEnvVarsMap(device) 473 | containerEnvVars := getEnvVarsMapFromK8sAPI(container.Env) 474 | if !reflect.DeepEqual(envVarsMap, containerEnvVars) { 475 | msg := fmt.Sprintf("Updating CEosLabDevice %s pod environment variables, new: %v, old: %v", 476 | device.Name, envVarsMap, containerEnvVars) 477 | log.Info(msg) 478 | // Environment variables can only be changed by restarting the pod 479 | // Delete the pod, and requeue to recreate 480 | r.Delete(ctx, devicePod) 481 | r.updateDeviceReconciling(ctx, device, msg) 482 | return requeue, nil 483 | } 484 | 485 | // Ensure the pod image is the same as the spec 486 | specImage := getImage(device) 487 | containerImage := container.Image 488 | if specImage != containerImage { 489 | msg := fmt.Sprintf("Updating CEosLabDevice %s pod image, new: %s, old: %s", 490 | device.Name, specImage, containerImage) 491 | log.Info(msg) 492 | // Image can only be changed by restarting the pod 493 | // Delete the pod, and requeue to recreate 494 | r.Delete(ctx, devicePod) 495 | r.updateDeviceReconciling(ctx, device, msg) 496 | return requeue, nil 497 | } 498 | 499 | // Ensure the pod resource requirements are same as the spec 500 | specResourceRequirements, err := getResourceMap(device) 501 | containerResourceRequirements := getResourceMapFromK8sAPI(&container.Resources) 502 | if err != nil { 503 | errMsg := fmt.Sprintf("Failed to validate CEosLabDevice %s pod requirements, new: %v, old: %v", 504 | device.Name, device.Spec.Resources, containerResourceRequirements) 505 | log.Error(err, errMsg) 506 | r.updateDeviceFail(ctx, device, errMsg) 507 | return noRequeue, nil 508 | } 509 | if !reflect.DeepEqual(specResourceRequirements, containerResourceRequirements) { 510 | msg := fmt.Sprintf("Updating CEosLabDevice %s pod resource requirements, new: %v, old: %v", 511 | device.Name, specResourceRequirements, containerResourceRequirements) 512 | log.Info(msg) 513 | // Resource requirements can only be changed by restarting the pod 514 | // Delete the pod, and requeue to recreate 515 | r.Delete(ctx, devicePod) 516 | r.updateDeviceReconciling(ctx, device, msg) 517 | return requeue, nil 518 | } 519 | 520 | // Ensure the pod args are the same as the spec 521 | specArgs := getArgs(device, envVarsMap) 522 | containerArgs := container.Args 523 | if !reflect.DeepEqual(specArgs, containerArgs) { 524 | msg := fmt.Sprintf("Updating CEosLabDevice %s pod arguments, new: %v, old: %v", 525 | device.Name, specArgs, containerArgs) 526 | log.Info(msg) 527 | // Update pod 528 | // Pod args can only be changed by restarting the pod 529 | // Delete the pod, and requeue to recreate 530 | r.Delete(ctx, devicePod) 531 | r.updateDeviceReconciling(ctx, device, msg) 532 | return requeue, nil 533 | } 534 | 535 | // Ensure the pod init container args are correct. 536 | initContainer := devicePod.Spec.InitContainers[0] 537 | specInitContainerArgs := getInitContainerArgs(device) 538 | podInitContainerArgs := initContainer.Args 539 | if !reflect.DeepEqual(specInitContainerArgs, podInitContainerArgs) { 540 | msg := fmt.Sprintf("Updating CEosLabDevice %s pod init container arguments, new: %s, old: %s", 541 | device.Name, specInitContainerArgs, podInitContainerArgs) 542 | log.Info(msg) 543 | // Init container args can only be changed by restarting the pod 544 | // Delete the pod, and requeue to recreate 545 | r.Delete(ctx, devicePod) 546 | r.updateDeviceReconciling(ctx, device, msg) 547 | return requeue, nil 548 | } 549 | 550 | // Ensure the pod init container image is correct 551 | specInitContainerImage := getInitContainerImage(device) 552 | podInitContainerImage := initContainer.Image 553 | if specInitContainerImage != podInitContainerImage { 554 | msg := fmt.Sprintf("Updating CEosLabDevice %s pod init container image, new: %s, old: %s", 555 | device.Name, specInitContainerImage, podInitContainerImage) 556 | log.Info(msg) 557 | // Init container image can only be changed by restarting the pod 558 | // Delete the pod, and requeue to recreate 559 | r.Delete(ctx, devicePod) 560 | r.updateDeviceReconciling(ctx, device, msg) 561 | return requeue, nil 562 | } 563 | 564 | // Ensure services are the same as the spec 565 | specServiceMap, err := getServiceMap(device) 566 | if err != nil { 567 | return noRequeue, err 568 | } 569 | containerServiceMap := getServiceMapFromK8sAPI(deviceService) 570 | if !reflect.DeepEqual(specServiceMap, containerServiceMap) { 571 | msg := fmt.Sprintf("Updating CEosLabDevice %s pod services, new: %v, old: %v", 572 | device.Name, specServiceMap, containerServiceMap) 573 | log.Info(msg) 574 | // We could update services in place but this would make it harder to handle 575 | // the delete case. The fallout from just deleting and recreating them is 576 | // comparatively minimal. 577 | r.Delete(ctx, deviceService) 578 | // Update status 579 | r.updateDeviceReconciling(ctx, device, msg) 580 | return requeue, nil 581 | } 582 | 583 | // Ensure startup probe agents are the same as the spec 584 | specWaitForAgents := device.Spec.WaitForAgents 585 | containerWaitForAgents := getWaitForAgentsFromK8sAPI(container.StartupProbe) 586 | confed := len(specWaitForAgents)+len(containerWaitForAgents) > 0 587 | if confed && !reflect.DeepEqual(specWaitForAgents, containerWaitForAgents) { 588 | msg := fmt.Sprintf("Updating CEosLabDevice %s agents to wait for at boot, new %v, old: %v:", 589 | device.Name, specWaitForAgents, containerWaitForAgents) 590 | log.Info(msg) 591 | // Update pod 592 | // Pod args can only be changed by restarting the pod 593 | // Delete the pod, and requeue to recreate 594 | // At this point it's getting a bit silly, but let's keep the standard. 595 | r.Delete(ctx, devicePod) 596 | // Update status 597 | r.updateDeviceReconciling(ctx, device, msg) 598 | return requeue, nil 599 | } 600 | 601 | // Device reconciled 602 | log.Info(fmt.Sprintf("CEosLabDevice %s reconciled", device.Name)) 603 | r.updateDeviceSuccess(ctx, device) 604 | return noRequeue, nil 605 | } 606 | 607 | // SetupWithManager sets up the controller with the Manager. 608 | func (r *CEosLabDeviceReconciler) SetupWithManager(mgr ctrl.Manager) error { 609 | return ctrl.NewControllerManagedBy(mgr). 610 | For(&ceoslabv1alpha1.CEosLabDevice{}). 611 | Owns(&corev1.Pod{}). 612 | Owns(&corev1.Service{}). 613 | Owns(&corev1.ConfigMap{}). 614 | Owns(&corev1.Secret{}). 615 | WithOptions(rctrl.Options{ 616 | MaxConcurrentReconciles: runtime.NumCPU(), 617 | }). 618 | Complete(r) 619 | } 620 | 621 | // Reconciliation helpers 622 | 623 | type makeObjectFn func(client.Object) error 624 | 625 | func (r *CEosLabDeviceReconciler) getOrCreateObject(ctx context.Context, device *ceoslabv1alpha1.CEosLabDevice, name string, makeObject makeObjectFn, object client.Object) error { 626 | log := kLog.FromContext(ctx) 627 | err := r.Get(ctx, types.NamespacedName{Name: name, Namespace: device.Namespace}, object) 628 | if err != nil && errors.IsNotFound(err) { 629 | // Create new k8s object 630 | msg := fmt.Sprintf("Creating %s for CEosLabDevice %s", name, device.Name) 631 | log.Info(msg) 632 | doCreateObjectError := func() { 633 | errMsg := fmt.Sprintf("Failed to create %s for CEosLabDevice %s", 634 | object, device.Name) 635 | log.Error(err, errMsg) 636 | r.updateDeviceFail(ctx, device, errMsg) 637 | } 638 | err = makeObject(object) 639 | if err != nil { 640 | doCreateObjectError() 641 | return err 642 | } 643 | object.SetName(name) 644 | object.SetNamespace(device.Namespace) 645 | ctrl.SetControllerReference(device, object, r.Scheme) 646 | err = r.Create(ctx, object) 647 | if err != nil { 648 | doCreateObjectError() 649 | return err 650 | } 651 | log.Info(fmt.Sprintf("Created %s for CEosLabDevice %s", name, device.Name)) 652 | } else if err != nil { 653 | errMsg := fmt.Sprintf("Failed to get %s for CEosLabDevice %s", name, device.Name) 654 | log.Error(err, errMsg) 655 | r.updateDeviceFail(ctx, device, errMsg) 656 | return err 657 | } 658 | return nil 659 | } 660 | 661 | // ConfigMaps and Secrets 662 | 663 | func getSelfSignedCert(secret *corev1.Secret, config ceoslabv1alpha1.SelfSignedCertConfig, deviceName string) error { 664 | key, err := rsa.GenerateKey(rand.Reader, int(config.KeySize)) 665 | if err != nil { 666 | return err 667 | } 668 | keyPem := pem.EncodeToMemory(&pem.Block{ 669 | Type: "RSA PRIVATE KEY", 670 | Bytes: x509.MarshalPKCS1PrivateKey(key), 671 | }) 672 | commonName := config.CommonName 673 | if commonName == "" { 674 | commonName = deviceName 675 | } 676 | template := &x509.Certificate{ 677 | BasicConstraintsValid: true, 678 | ExtKeyUsage: []x509.ExtKeyUsage{x509.ExtKeyUsageServerAuth}, 679 | KeyUsage: x509.KeyUsageKeyEncipherment | x509.KeyUsageDigitalSignature, 680 | Subject: pkix.Name{ 681 | Organization: []string{"Arista Networks"}, 682 | CommonName: commonName, 683 | }, 684 | SerialNumber: big.NewInt(1), 685 | NotBefore: time.Now(), 686 | NotAfter: time.Now().Add(time.Hour * 24 * 365), 687 | } 688 | cert, err := x509.CreateCertificate(rand.Reader, template, template, &key.PublicKey, key) 689 | if err != nil { 690 | return err 691 | } 692 | certPem := pem.EncodeToMemory(&pem.Block{ 693 | Type: "CERTIFICATE", 694 | Bytes: cert, 695 | }) 696 | secret.Data = map[string][]byte{ 697 | config.CertName: certPem, 698 | config.KeyName: keyPem, 699 | } 700 | return nil 701 | } 702 | 703 | func getRcEos(configMap *corev1.ConfigMap, configs []ceoslabv1alpha1.SelfSignedCertConfig) { 704 | // Generate file to run at boot. It waits for agents to be warm and then 705 | // copies certificates to an in-memory filesystem. The `Cli -c` command expects 706 | // literal carriage returns. Sample output: 707 | // #!/bin/bash 708 | // { 709 | // wfw ConfigAgent 710 | // Cli -Ac 'enable 711 | // copy file:/mnt/flash/cert.pem certificate:cert.pem 712 | // copy file:/mnt/flash/key.pem sslkey:key.pem 713 | // ' 714 | // } 2> /mnt/flash/rc.eos.log >&2 & 715 | buffer := &bytes.Buffer{} 716 | writeLn := func(txt string) { 717 | buffer.Write([]byte(txt + "\n")) 718 | } 719 | writeLn("#!/bin/bash") 720 | writeLn("{") 721 | writeLn("wfw ConfigAgent") 722 | writeLn("Cli -Ac 'enable") 723 | for _, certConfig := range configs { 724 | writeLn(fmt.Sprintf("copy file:/mnt/flash/%s certificate:%s", 725 | certConfig.CertName, certConfig.CertName)) 726 | writeLn(fmt.Sprintf("copy file:/mnt/flash/%s sslkey:%s", 727 | certConfig.KeyName, certConfig.KeyName)) 728 | } 729 | writeLn("'") 730 | writeLn("} 2>/mnt/flash/rc.eos.log >&2 &") 731 | script := buffer.Bytes() 732 | // This file needs to be executable 733 | configMap.ObjectMeta.Annotations = map[string]string{"permissions": "775"} 734 | configMap.BinaryData = map[string][]byte{"rc.eos": script} 735 | return 736 | } 737 | 738 | type jsonIntfMapping struct { 739 | // Internal structure to marshal to JSON kernel device to interface mapping 740 | EthernetIntf map[string]string `json:"EthernetIntf,omitempty"` 741 | ManagementIntf map[string]string `json:"ManagementIntf,omitempty"` 742 | } 743 | 744 | func getJsonIntfMapping(configMap *corev1.ConfigMap, intfMapping map[string]string) error { 745 | jsonIntfMapping := jsonIntfMapping{ 746 | EthernetIntf: map[string]string{}, 747 | ManagementIntf: map[string]string{}, 748 | } 749 | for kernelDevice, eosInterface := range intfMapping { 750 | if ethIntfRe.MatchString(eosInterface) { 751 | jsonIntfMapping.EthernetIntf[kernelDevice] = eosInterface 752 | } else if mgmtIntfRe.MatchString(eosInterface) { 753 | jsonIntfMapping.ManagementIntf[kernelDevice] = eosInterface 754 | } else { 755 | err := fmt.Errorf("Unrecognized EOS interface %s", eosInterface) 756 | return err 757 | } 758 | } 759 | jsonIntfMappingBytes, err := json.Marshal(jsonIntfMapping) 760 | if err != nil { 761 | return err 762 | } 763 | configMap.BinaryData = map[string][]byte{"EosIntfMapping.json": jsonIntfMappingBytes} 764 | return nil 765 | } 766 | 767 | func getToggleOverrides(configMap *corev1.ConfigMap, toggleOverrides map[string]bool) { 768 | buffer := &bytes.Buffer{} 769 | for toggle, enabled := range toggleOverrides { 770 | if enabled { 771 | buffer.Write([]byte(toggle + "=1\n")) 772 | } else { 773 | buffer.Write([]byte(toggle + "=0\n")) 774 | } 775 | } 776 | overrides := buffer.Bytes() 777 | configMap.BinaryData = map[string][]byte{"toggle_override": overrides} 778 | return 779 | } 780 | 781 | // Pods 782 | 783 | func getPod(pod *corev1.Pod, device *ceoslabv1alpha1.CEosLabDevice, secretsAndConfigMaps map[string]client.Object) error { 784 | labels := getLabels(device) 785 | envVarsMap := getEnvVarsMap(device) 786 | env := getEnvVarsAPI(device, envVarsMap) 787 | command := getCommand(device) 788 | args := getArgs(device, envVarsMap) 789 | image := getImage(device) 790 | initImage := getInitContainerImage(device) 791 | resourceMap, err := getResourceMap(device) 792 | if err != nil { 793 | return err 794 | } 795 | volumes := getVolumes(secretsAndConfigMaps) 796 | volumeMounts := getVolumeMounts(secretsAndConfigMaps) 797 | resources := getResourcesAPI(resourceMap) 798 | startupProbe := getStartupProbeAPI(device) 799 | pod.ObjectMeta.Labels = labels 800 | pod.Spec = corev1.PodSpec{ 801 | InitContainers: []corev1.Container{{ 802 | Name: fmt.Sprintf("init-%s", device.Name), 803 | Image: initImage, 804 | Args: []string{ 805 | fmt.Sprintf("%d", device.Spec.NumInterfaces+1), 806 | fmt.Sprintf("%d", device.Spec.Sleep), 807 | }, 808 | ImagePullPolicy: "IfNotPresent", 809 | }}, 810 | Containers: []corev1.Container{{ 811 | Image: image, 812 | Name: "ceos", 813 | Command: command, 814 | Args: args, 815 | Env: env, 816 | Resources: resources, 817 | ImagePullPolicy: "IfNotPresent", 818 | VolumeMounts: volumeMounts, 819 | // Run container in privileged mode 820 | SecurityContext: &corev1.SecurityContext{Privileged: pointer.Bool(true)}, 821 | // Check if agents are warm 822 | StartupProbe: startupProbe, 823 | }}, 824 | TerminationGracePeriodSeconds: pointer.Int64(0), 825 | Volumes: volumes, 826 | } 827 | return nil 828 | } 829 | 830 | func getEnvVarsMap(device *ceoslabv1alpha1.CEosLabDevice) map[string]string { 831 | envVars := map[string]string{} 832 | for k, v := range defaultEnvVars { 833 | envVars[k] = v 834 | } 835 | for varname, val := range device.Spec.EnvVar { 836 | // Add new environment variables, possibly overriding defaults 837 | envVars[varname] = val 838 | } 839 | return envVars 840 | } 841 | 842 | func getEnvVarsAPI(device *ceoslabv1alpha1.CEosLabDevice, envVarsMap map[string]string) []corev1.EnvVar { 843 | varnames := []string{} 844 | for varname := range envVarsMap { 845 | varnames = append(varnames, varname) 846 | } 847 | // Sort the environment variables. It's important to have deterministic output for 848 | // testing purposes. 849 | sort.Strings(varnames) 850 | envVars := []corev1.EnvVar{} 851 | for _, varname := range varnames { 852 | envVars = append(envVars, corev1.EnvVar{ 853 | Name: varname, 854 | Value: envVarsMap[varname], 855 | }) 856 | } 857 | return envVars 858 | } 859 | 860 | func getEnvVarsMapFromK8sAPI(envVarsAPI []corev1.EnvVar) map[string]string { 861 | envVars := map[string]string{} 862 | for _, envvar := range envVarsAPI { 863 | envVars[envvar.Name] = envvar.Value 864 | } 865 | return envVars 866 | } 867 | 868 | func getCommand(device *ceoslabv1alpha1.CEosLabDevice) []string { 869 | command := []string{CEOS_COMMAND} 870 | return command 871 | } 872 | 873 | func getLabels(device *ceoslabv1alpha1.CEosLabDevice) map[string]string { 874 | labels := map[string]string{ 875 | // Defaults 876 | "app": device.Name, 877 | "topo": device.Namespace, 878 | } 879 | for label, value := range device.Labels { 880 | // Extra labels attached to CR, such as model, version, and OS 881 | labels[label] = value 882 | } 883 | return labels 884 | } 885 | 886 | func getImage(device *ceoslabv1alpha1.CEosLabDevice) string { 887 | image := DEFAULT_CEOS_IMAGE 888 | if specImage := device.Spec.Image; specImage != "" { 889 | image = specImage 890 | } 891 | return image 892 | } 893 | 894 | func getArgs(device *ceoslabv1alpha1.CEosLabDevice, envVarsMap map[string]string) []string { 895 | varnames := []string{} 896 | for varname := range envVarsMap { 897 | varnames = append(varnames, varname) 898 | } 899 | // Sort the environment variables. It's important to have deterministic output for 900 | // testing purposes. 901 | sort.Strings(varnames) 902 | args := []string{} 903 | for _, varname := range varnames { 904 | val := envVarsMap[varname] 905 | args = append(args, "systemd.setenv="+varname+"="+val) 906 | } 907 | for _, arg := range device.Spec.Args { 908 | args = append(args, arg) 909 | } 910 | return args 911 | } 912 | 913 | func getInitContainerArgs(device *ceoslabv1alpha1.CEosLabDevice) []string { 914 | args := []string{ 915 | fmt.Sprintf("%d", device.Spec.NumInterfaces+1), 916 | fmt.Sprintf("%d", device.Spec.Sleep), 917 | } 918 | return args 919 | } 920 | 921 | func getInitContainerImage(device *ceoslabv1alpha1.CEosLabDevice) string { 922 | image := DEFAULT_INIT_CONTAINER_IMAGE 923 | if specImage := device.Spec.InitContainerImage; specImage != "" { 924 | image = specImage 925 | } 926 | return image 927 | } 928 | 929 | func getResourceMap(device *ceoslabv1alpha1.CEosLabDevice) (map[string]string, error) { 930 | resourceMap := map[string]string{} 931 | resourceSpec := device.Spec.Resources 932 | for resourceType, resourceStr := range resourceSpec { 933 | // Convert the resource into a quantity and back into a string. This is stupid, 934 | // but it lets us easily parse the requirement into a canonical form. Otherwise 935 | // we can't compare it to what's stored in etcd. 936 | asResource, err := resource.ParseQuantity(resourceStr) 937 | if err != nil { 938 | return nil, fmt.Errorf("Could not parse resource requirements: %s", resourceStr) 939 | } 940 | canonicalString := asResource.String() 941 | resourceMap[resourceType] = canonicalString 942 | } 943 | return resourceMap, nil 944 | } 945 | 946 | func getResourceMapFromK8sAPI(resources *corev1.ResourceRequirements) map[string]string { 947 | resourceMap := map[string]string{} 948 | for resourceType, asResource := range resources.Requests { 949 | canonicalString := asResource.String() 950 | resourceMap[string(resourceType)] = canonicalString 951 | } 952 | return resourceMap 953 | } 954 | 955 | func getResourcesAPI(resourceMap map[string]string) corev1.ResourceRequirements { 956 | requirements := corev1.ResourceRequirements{ 957 | Requests: map[corev1.ResourceName]resource.Quantity{}, 958 | } 959 | for resourceType, resourceStr := range resourceMap { 960 | // resourceStr has already been parsed into a canonical form, so this should never fail. 961 | requirements.Requests[corev1.ResourceName(resourceType)] = resource.MustParse(resourceStr) 962 | } 963 | return requirements 964 | } 965 | 966 | func volumeName(configMapName string) string { 967 | return "volume-" + configMapName 968 | } 969 | 970 | func getVolumes(secretsAndConfigMaps map[string]client.Object) []corev1.Volume { 971 | volumeNames := []string{} 972 | for name := range secretsAndConfigMaps { 973 | volumeNames = append(volumeNames, name) 974 | } 975 | sort.Strings(volumeNames) 976 | volumes := []corev1.Volume{} 977 | for _, name := range volumeNames { 978 | secretOrConfigMap := secretsAndConfigMaps[name] 979 | var permissions *int32 = nil 980 | if permsStr, ok := secretOrConfigMap.GetAnnotations()["permissions"]; ok { 981 | // Permissions are represented in octal 982 | perms, err := strconv.ParseInt(permsStr, 8, 32) 983 | if err != nil { 984 | // This is set by the controller so this would be unexpected. 985 | panic(fmt.Sprintf("Could not parse permissions %s to int", permsStr)) 986 | } 987 | permissions = pointer.Int32(int32(perms)) 988 | } 989 | volumeSource := corev1.VolumeSource{} 990 | switch secretOrConfigMap.(type) { 991 | case *corev1.ConfigMap: 992 | volumeSource = corev1.VolumeSource{ 993 | ConfigMap: &corev1.ConfigMapVolumeSource{ 994 | LocalObjectReference: corev1.LocalObjectReference{ 995 | Name: name, 996 | }, 997 | }, 998 | } 999 | if permissions != nil { 1000 | volumeSource.ConfigMap.DefaultMode = permissions 1001 | } 1002 | case *corev1.Secret: 1003 | volumeSource = corev1.VolumeSource{ 1004 | Secret: &corev1.SecretVolumeSource{ 1005 | SecretName: name, 1006 | }, 1007 | } 1008 | if permissions != nil { 1009 | volumeSource.Secret.DefaultMode = permissions 1010 | } 1011 | } 1012 | volume := corev1.Volume{ 1013 | Name: volumeName(name), 1014 | VolumeSource: volumeSource, 1015 | } 1016 | volumes = append(volumes, volume) 1017 | } 1018 | return volumes 1019 | } 1020 | 1021 | func getVolumeMounts(secretsAndConfigMaps map[string]client.Object) []corev1.VolumeMount { 1022 | volumeNames := []string{} 1023 | for name := range secretsAndConfigMaps { 1024 | volumeNames = append(volumeNames, name) 1025 | } 1026 | sort.Strings(volumeNames) 1027 | mounts := []corev1.VolumeMount{} 1028 | for _, name := range volumeNames { 1029 | secretOrConfigMap := secretsAndConfigMaps[name] 1030 | filenames := []string{} 1031 | switch v := secretOrConfigMap.(type) { 1032 | case *corev1.ConfigMap: 1033 | for filename := range v.BinaryData { 1034 | filenames = append(filenames, filename) 1035 | } 1036 | for filename := range v.Data { 1037 | filenames = append(filenames, filename) 1038 | } 1039 | case *corev1.Secret: 1040 | for filename := range v.Data { 1041 | filenames = append(filenames, filename) 1042 | } 1043 | for filename := range v.StringData { 1044 | filenames = append(filenames, filename) 1045 | } 1046 | } 1047 | sort.Strings(filenames) 1048 | for _, filename := range filenames { 1049 | mounts = append(mounts, corev1.VolumeMount{ 1050 | Name: volumeName(name), 1051 | MountPath: "/mnt/flash/" + filename, 1052 | SubPath: filename, 1053 | }) 1054 | } 1055 | } 1056 | return mounts 1057 | } 1058 | 1059 | func getStartupProbeAPI(device *ceoslabv1alpha1.CEosLabDevice) *corev1.Probe { 1060 | agents := device.Spec.WaitForAgents 1061 | command := append([]string{"wfw", "-t", "5"}, agents...) 1062 | return &corev1.Probe{ 1063 | ProbeHandler: corev1.ProbeHandler{ 1064 | Exec: &corev1.ExecAction{ 1065 | Command: command, 1066 | }, 1067 | }, 1068 | // Try to boot for up to two minutes 1069 | TimeoutSeconds: 5, 1070 | PeriodSeconds: 5, 1071 | FailureThreshold: 24, 1072 | } 1073 | } 1074 | 1075 | func getWaitForAgentsFromK8sAPI(startupProbe *corev1.Probe) []string { 1076 | command := startupProbe.ProbeHandler.Exec.Command 1077 | // [ "wfw", "-t", "5", agents... ] 1078 | return command[3:] 1079 | } 1080 | 1081 | // Services 1082 | 1083 | func getService(service *corev1.Service, device *ceoslabv1alpha1.CEosLabDevice) error { 1084 | serviceMap, err := getServiceMap(device) 1085 | if err != nil { 1086 | return err 1087 | } 1088 | servicePorts := getServicePortsAPI(serviceMap) 1089 | service.ObjectMeta.Labels = map[string]string{"pod": device.Name} 1090 | service.Spec = corev1.ServiceSpec{ 1091 | Ports: servicePorts, 1092 | Selector: map[string]string{ 1093 | "app": device.Name, 1094 | }, 1095 | Type: "LoadBalancer", 1096 | } 1097 | return nil 1098 | } 1099 | 1100 | func getServiceMap(device *ceoslabv1alpha1.CEosLabDevice) (map[string]ceoslabv1alpha1.PortConfig, error) { 1101 | serviceMap := map[string]ceoslabv1alpha1.PortConfig{} 1102 | // Handle services in a deterministic order in case we ignore a service that shares a port with 1103 | // another. Otherwise a future reconcilation may elide a different one and create a discrepancy. 1104 | serviceNames := []string{} 1105 | for service := range device.Spec.Services { 1106 | serviceNames = append(serviceNames, service) 1107 | } 1108 | sort.Strings(serviceNames) 1109 | portMappingOutIn := map[int32]int32{} 1110 | portMappingInOut := map[int32]int32{} 1111 | for _, service := range serviceNames { 1112 | serviceConfig := device.Spec.Services[service] 1113 | for _, portConfig := range serviceConfig.TCPPorts { 1114 | effectivePortConfig := portConfig 1115 | if out := portConfig.Out; out == 0 { 1116 | // Outside port not set, default to inside. 1117 | effectivePortConfig.Out = portConfig.In 1118 | } 1119 | // Check if we've already forwarded this port 1120 | in, present := portMappingOutIn[effectivePortConfig.Out] 1121 | if present && in != effectivePortConfig.In { 1122 | // We already mapped this outside port to a different inside port 1123 | return nil, fmt.Errorf("Service %s wants to map outside port %d to %d but it's already mapped to %d", 1124 | service, effectivePortConfig.Out, effectivePortConfig.In, in) 1125 | } 1126 | out, present := portMappingInOut[effectivePortConfig.In] 1127 | if present && out != effectivePortConfig.Out { 1128 | // Wr aleady mapped this inside port to a different outside port 1129 | return nil, fmt.Errorf("Service %s wants to map inside port %d to %d but it's already mapped to %d", 1130 | service, effectivePortConfig.In, effectivePortConfig.Out, out) 1131 | } 1132 | if present { 1133 | // Two services have the same port mapping. This is OK, we just need to ignore this one. 1134 | continue 1135 | } 1136 | portMappingOutIn[effectivePortConfig.Out] = effectivePortConfig.In 1137 | portMappingInOut[effectivePortConfig.In] = effectivePortConfig.Out 1138 | serviceName := strings.ToLower(fmt.Sprintf("%s%d", service, effectivePortConfig.In)) 1139 | serviceMap[serviceName] = effectivePortConfig 1140 | } 1141 | } 1142 | return serviceMap, nil 1143 | } 1144 | 1145 | func getServiceMapFromK8sAPI(service *corev1.Service) map[string]ceoslabv1alpha1.PortConfig { 1146 | serviceMap := map[string]ceoslabv1alpha1.PortConfig{} 1147 | for _, v := range service.Spec.Ports { 1148 | serviceMap[v.Name] = ceoslabv1alpha1.PortConfig{ 1149 | // We always set TargetPort and Port (see getServiceMap) so we can pull 1150 | // out these values naively. 1151 | In: int32(v.TargetPort.IntValue()), 1152 | Out: int32(v.Port), 1153 | } 1154 | } 1155 | return serviceMap 1156 | } 1157 | 1158 | func getServicePortsAPI(serviceMap map[string]ceoslabv1alpha1.PortConfig) []corev1.ServicePort { 1159 | services := []string{} 1160 | for service := range serviceMap { 1161 | services = append(services, service) 1162 | } 1163 | // Sort the services. It's important to have deterministic output for testing purposes. 1164 | sort.Strings(services) 1165 | ports := []corev1.ServicePort{} 1166 | for _, serviceName := range services { 1167 | servicePort := serviceMap[serviceName] 1168 | // serviceName is the service name concatenated with the protocol type 1169 | ports = append(ports, corev1.ServicePort{ 1170 | Name: serviceName, 1171 | Protocol: corev1.ProtocolTCP, 1172 | // We always set TargetPort and Port (see getServiceMap) so we can push 1173 | // in these values naively. 1174 | Port: int32(servicePort.Out), 1175 | TargetPort: intstr.FromInt(int(servicePort.In)), 1176 | }) 1177 | } 1178 | return ports 1179 | } 1180 | --------------------------------------------------------------------------------