├── config ├── manager │ ├── kustomization.yaml │ └── manager.yaml ├── prometheus │ ├── kustomization.yaml │ └── monitor.yaml ├── certmanager │ ├── kustomization.yaml │ ├── kustomizeconfig.yaml │ └── certificate.yaml ├── webhook │ ├── kustomization.yaml │ ├── service.yaml │ └── kustomizeconfig.yaml ├── rbac │ ├── auth_proxy_client_clusterrole.yaml │ ├── role_binding.yaml │ ├── auth_proxy_role_binding.yaml │ ├── auth_proxy_service.yaml │ ├── leader_election_role_binding.yaml │ ├── auth_proxy_role.yaml │ ├── kustomization.yaml │ ├── clusterspec_viewer_role.yaml │ ├── machinespec_viewer_role.yaml │ ├── existinginfracluster_viewer_role.yaml │ ├── existinginframachine_viewer_role.yaml │ ├── clusterspec_editor_role.yaml │ ├── machinespec_editor_role.yaml │ ├── existinginfracluster_editor_role.yaml │ ├── existinginframachine_editor_role.yaml │ ├── leader_election_role.yaml │ └── role.yaml ├── samples │ ├── baremetalproviderspec_v1alpha1_clusterspec.yaml │ ├── baremetalproviderspec_v1alpha1_machinespec.yaml │ ├── cluster.weave.works_v1alpha3_existinginfracluster.yaml │ └── cluster.weave.works_v1alpha3_existinginframachine.yaml ├── crd │ ├── patches │ │ ├── cainjection_in_clusterspecs.yaml │ │ ├── cainjection_in_machinespecs.yaml │ │ ├── cainjection_in_existinginfraclusters.yaml │ │ ├── cainjection_in_existinginframachines.yaml │ │ ├── webhook_in_clusterspecs.yaml │ │ ├── webhook_in_machinespecs.yaml │ │ ├── webhook_in_existinginfraclusters.yaml │ │ └── webhook_in_existinginframachines.yaml │ ├── kustomizeconfig.yaml │ ├── kustomization.yaml │ ├── cluster.weave.works_existinginframachiness.yaml │ └── bases │ │ └── cluster.weave.works_existinginframachines.yaml └── default │ ├── manager_webhook_patch.yaml │ ├── webhookcainjection_patch.yaml │ ├── manager_auth_proxy_patch.yaml │ └── kustomization.yaml ├── templates └── image_tag.template ├── pkg ├── utilities │ ├── version │ │ ├── generated.go │ │ ├── version_test.go │ │ └── version.go │ ├── object │ │ └── String.go │ ├── kubeadm │ │ ├── assets.go │ │ ├── bootstrap_token_test.go │ │ ├── bootstrap_token.go │ │ ├── joincmd.go │ │ └── joincmd_test.go │ ├── encoding │ │ └── encoding.go │ ├── path │ │ └── path.go │ ├── fixeddate │ │ └── fixeddate.go │ ├── envcfg │ │ └── environment_config.go │ └── ssh │ │ └── ssh.go ├── apis │ └── wksprovider │ │ ├── manifests │ │ ├── doc.go │ │ ├── yaml │ │ │ ├── 01_namespace.yaml │ │ │ ├── 03_secrets.yaml │ │ │ ├── 05_sealed_secret_crd.yaml │ │ │ ├── 04_capi_controller.yaml │ │ │ └── 04_controller.yaml │ │ ├── manifests_dev.go │ │ └── assets_generate.go │ │ └── machine │ │ ├── crds │ │ ├── doc.go │ │ ├── crds_dev.go │ │ └── assets_generate.go │ │ ├── config │ │ ├── kubelet.go │ │ ├── kubeproxy │ │ │ └── kubeproxy.go │ │ └── kubeadm │ │ │ └── kubeadm_test.go │ │ └── scripts │ │ └── run.go ├── plan │ ├── resource │ │ ├── common.go │ │ ├── proxy.go │ │ ├── state.go │ │ ├── state_test.go │ │ ├── plan_serialization_test.go │ │ ├── parsing.go │ │ ├── base.go │ │ ├── run_test.go │ │ ├── parsing_test.go │ │ ├── run.go │ │ ├── kubectl_annotate.go │ │ ├── dir.go │ │ ├── dpkg.go │ │ ├── deb.go │ │ ├── apt.go │ │ ├── kubectl_wait.go │ │ ├── kube_secret.go │ │ ├── service.go │ │ ├── file.go │ │ ├── kubeadm_join.go │ │ ├── rpm_test.go │ │ └── rpm.go │ ├── runners │ │ ├── sudo │ │ │ └── sudo.go │ │ └── ssh │ │ │ └── ssh.go │ ├── runner_local.go │ ├── runner_local_test.go │ ├── recipe │ │ ├── renew_kubeadm_certs.go │ │ ├── install_plans_test.go │ │ └── upgrade_plans.go │ ├── graph_test.go │ ├── builder_test.go │ ├── builder.go │ ├── resource_test.go │ ├── graph.go │ └── state_test.go ├── kubernetes │ ├── version.go │ ├── version_test.go │ └── drain │ │ └── facade.go ├── specs │ └── specs_test.go ├── cluster │ └── machine │ │ └── machine_test.go ├── scheme │ └── scheme.go └── flavors │ └── eksd │ ├── eksd_test.go │ └── eksd.go ├── local-repository └── infrastructure-existinginfra │ └── v0.1.0 │ └── metadata.yaml ├── .codecov.yml ├── .goreleaser.yml ├── .gitignore ├── PROJECT ├── docs └── release-process.md ├── golangci.yaml.save ├── hack ├── boilerplate.go.txt └── image-tag ├── .golangci.yml ├── test └── plan │ └── testutils │ ├── mock_test.go │ └── runner_mock.go ├── tools └── image-tag ├── apis ├── baremetalproviderspec │ └── v1alpha1 │ │ ├── doc.go │ │ ├── machinespec_types.go │ │ ├── groupversion_info.go │ │ ├── conversion.go │ │ └── clusterspec_types.go └── cluster.weave.works │ └── v1alpha3 │ ├── groupversion_info.go │ └── existinginframachine_types.go ├── Dockerfile ├── .github └── workflows │ ├── build.yaml │ └── goreleaser.yaml ├── controllers └── cluster.weave.works │ ├── helpers.go │ ├── existinginfrabootstrap_controller.go │ ├── suite_test.go │ └── existinginframachine_controller_test.go ├── go.mod ├── .circleci └── config.yml └── Makefile /config/manager/kustomization.yaml: -------------------------------------------------------------------------------- 1 | resources: 2 | - manager.yaml 3 | -------------------------------------------------------------------------------- /config/prometheus/kustomization.yaml: -------------------------------------------------------------------------------- 1 | resources: 2 | - monitor.yaml 3 | -------------------------------------------------------------------------------- /templates/image_tag.template: -------------------------------------------------------------------------------- 1 | package version 2 | 3 | const ImageTag = -------------------------------------------------------------------------------- /pkg/utilities/version/generated.go: -------------------------------------------------------------------------------- 1 | package version 2 | 3 | const ImageTag = "v0.2.5" 4 | -------------------------------------------------------------------------------- /config/certmanager/kustomization.yaml: -------------------------------------------------------------------------------- 1 | resources: 2 | - certificate.yaml 3 | 4 | configurations: 5 | - kustomizeconfig.yaml 6 | -------------------------------------------------------------------------------- /config/webhook/kustomization.yaml: -------------------------------------------------------------------------------- 1 | resources: 2 | - manifests.yaml 3 | - service.yaml 4 | 5 | configurations: 6 | - kustomizeconfig.yaml 7 | -------------------------------------------------------------------------------- /pkg/apis/wksprovider/manifests/doc.go: -------------------------------------------------------------------------------- 1 | //go:generate go run -tags=dev assets_generate.go 2 | 3 | // Package manifests contains wksctl's manifests. 4 | package manifests 5 | -------------------------------------------------------------------------------- /pkg/apis/wksprovider/machine/crds/doc.go: -------------------------------------------------------------------------------- 1 | //go:generate go run -tags=dev assets_generate.go 2 | 3 | // Package crds contains cluster-api-provider-existinginfra's crds. 4 | package crds 5 | -------------------------------------------------------------------------------- /pkg/apis/wksprovider/manifests/yaml/01_namespace.yaml: -------------------------------------------------------------------------------- 1 | --- 2 | apiVersion: v1 3 | kind: Namespace 4 | metadata: 5 | labels: 6 | controller-tools.k8s.io: "1.0" 7 | name: system 8 | -------------------------------------------------------------------------------- /local-repository/infrastructure-existinginfra/v0.1.0/metadata.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: clusterctl.cluster.x-k8s.io/v1alpha3 2 | kind: Metadata 3 | releaseSeries: 4 | - major: 0 5 | minor: 1 6 | contract: v1alpha3 7 | -------------------------------------------------------------------------------- /pkg/plan/resource/common.go: -------------------------------------------------------------------------------- 1 | package resource 2 | 3 | type PkgType string 4 | 5 | const ( 6 | PkgTypeDeb PkgType = "Deb" 7 | PkgTypeRPM PkgType = "RPM" 8 | PkgTypeRHEL PkgType = "RHEL" 9 | ) 10 | -------------------------------------------------------------------------------- /pkg/apis/wksprovider/manifests/yaml/03_secrets.yaml: -------------------------------------------------------------------------------- 1 | --- 2 | apiVersion: v1 3 | kind: Secret 4 | metadata: 5 | name: wks-controller-secrets 6 | namespace: system 7 | type: Opaque 8 | data: 9 | sshKey: "" 10 | -------------------------------------------------------------------------------- /config/rbac/auth_proxy_client_clusterrole.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: rbac.authorization.k8s.io/v1beta1 2 | kind: ClusterRole 3 | metadata: 4 | name: metrics-reader 5 | rules: 6 | - nonResourceURLs: ["/metrics"] 7 | verbs: ["get"] 8 | -------------------------------------------------------------------------------- /config/samples/baremetalproviderspec_v1alpha1_clusterspec.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: baremetalproviderspec/v1alpha1 2 | kind: ClusterSpec 3 | metadata: 4 | name: clusterspec-sample 5 | spec: 6 | # Add fields here 7 | foo: bar 8 | -------------------------------------------------------------------------------- /config/samples/baremetalproviderspec_v1alpha1_machinespec.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: baremetalproviderspec/v1alpha1 2 | kind: MachineSpec 3 | metadata: 4 | name: machinespec-sample 5 | spec: 6 | # Add fields here 7 | foo: bar 8 | -------------------------------------------------------------------------------- /pkg/kubernetes/version.go: -------------------------------------------------------------------------------- 1 | package kubernetes 2 | 3 | // DefaultVersion is the default Kubernetes version used by WKS. 4 | const ( 5 | DefaultVersion = "1.17.13" 6 | DefaultVersionsRange = ">=1.16.1 <=1.20.x" 7 | ) 8 | -------------------------------------------------------------------------------- /config/samples/cluster.weave.works_v1alpha3_existinginfracluster.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: cluster.weave.works/v1alpha3 2 | kind: ExistingInfraCluster 3 | metadata: 4 | name: existinginfracluster-sample 5 | spec: 6 | # Add fields here 7 | foo: bar 8 | -------------------------------------------------------------------------------- /config/samples/cluster.weave.works_v1alpha3_existinginframachine.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: cluster.weave.works/v1alpha3 2 | kind: ExistingInfraMachine 3 | metadata: 4 | name: existinginframachine-sample 5 | spec: 6 | # Add fields here 7 | foo: bar 8 | -------------------------------------------------------------------------------- /config/webhook/service.yaml: -------------------------------------------------------------------------------- 1 | 2 | apiVersion: v1 3 | kind: Service 4 | metadata: 5 | name: webhook-service 6 | namespace: system 7 | spec: 8 | ports: 9 | - port: 443 10 | targetPort: 9443 11 | selector: 12 | control-plane: controller-manager 13 | -------------------------------------------------------------------------------- /pkg/plan/resource/proxy.go: -------------------------------------------------------------------------------- 1 | package resource 2 | 3 | import "fmt" 4 | 5 | const unsetProxy = "unset http_proxy https_proxy HTTP_PROXY HTTPS_PROXY" 6 | 7 | func WithoutProxy(script string) string { 8 | return fmt.Sprintf("( %s && ( %s ) )", unsetProxy, script) 9 | } 10 | -------------------------------------------------------------------------------- /pkg/apis/wksprovider/machine/config/kubelet.go: -------------------------------------------------------------------------------- 1 | package config 2 | 3 | // KubeletConfig groups all options & flags which need to be passed to kubelet. 4 | type KubeletConfig struct { 5 | NodeIP string 6 | CloudProvider string 7 | ExtraArguments map[string]string 8 | } 9 | -------------------------------------------------------------------------------- /pkg/utilities/object/String.go: -------------------------------------------------------------------------------- 1 | package object 2 | 3 | // This type is used to make intent more clear when passing a simple string to something that expects an 4 | // instance of Stringer 5 | 6 | type String string 7 | 8 | func (s String) String() string { 9 | return string(s) 10 | } 11 | -------------------------------------------------------------------------------- /.codecov.yml: -------------------------------------------------------------------------------- 1 | coverage: 2 | range: "90..100" 3 | status: 4 | project: 5 | default: 6 | target: auto 7 | threshold: 50 8 | base: auto 9 | patch: off 10 | 11 | comment: 12 | require_changes: true 13 | branches: 14 | - "!docs" 15 | - "!release" 16 | -------------------------------------------------------------------------------- /pkg/utilities/kubeadm/assets.go: -------------------------------------------------------------------------------- 1 | package kubeadm 2 | 3 | // Tells which images to retrieve and where to retrieve them for DNS, Etcd, and Kubernetes 4 | type AssetDescription struct { 5 | ImageRepository string `json:"imageRepository,omitempty"` 6 | ImageTag string `json:"imageTag,omitempty"` 7 | } 8 | -------------------------------------------------------------------------------- /pkg/plan/resource/state.go: -------------------------------------------------------------------------------- 1 | package resource 2 | 3 | import ( 4 | "github.com/weaveworks/cluster-api-provider-existinginfra/pkg/plan" 5 | 6 | "github.com/fatih/structs" 7 | ) 8 | 9 | // ToState creates a new State using reflection on v. 10 | func ToState(v interface{}) plan.State { 11 | return structs.Map(v) 12 | } 13 | -------------------------------------------------------------------------------- /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: default 12 | namespace: system 13 | -------------------------------------------------------------------------------- /pkg/apis/wksprovider/manifests/manifests_dev.go: -------------------------------------------------------------------------------- 1 | // +build dev 2 | 3 | package manifests 4 | 5 | import ( 6 | "net/http" 7 | 8 | "github.com/weaveworks/cluster-api-provider-existinginfra/pkg/utilities/fixeddate" 9 | ) 10 | 11 | // Manifests contains existinginfra manifests. 12 | var Manifests http.FileSystem = fixeddate.Dir("yaml") 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: default 12 | namespace: system 13 | -------------------------------------------------------------------------------- /.goreleaser.yml: -------------------------------------------------------------------------------- 1 | # .goreleaser.yml 2 | project_name: cluster-api-provider-existinginfra 3 | 4 | # TODO: This should be populated with the build targets (e.g. docker images), so they are automatically uploaded. 5 | build: 6 | skip: true 7 | 8 | # This automatically closes any milestone named the same as the tag 9 | milestones: 10 | - close: true 11 | -------------------------------------------------------------------------------- /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-met-srv 7 | namespace: system 8 | spec: 9 | ports: 10 | - name: https 11 | port: 8443 12 | targetPort: https 13 | selector: 14 | control-plane: controller-manager 15 | -------------------------------------------------------------------------------- /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: default 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: ["authentication.k8s.io"] 7 | resources: 8 | - tokenreviews 9 | verbs: ["create"] 10 | - apiGroups: ["authorization.k8s.io"] 11 | resources: 12 | - subjectaccessreviews 13 | verbs: ["create"] 14 | -------------------------------------------------------------------------------- /pkg/apis/wksprovider/machine/crds/crds_dev.go: -------------------------------------------------------------------------------- 1 | // +build dev 2 | 3 | package crds 4 | 5 | import ( 6 | "net/http" 7 | 8 | "github.com/weaveworks/cluster-api-provider-existinginfra/pkg/utilities/fixeddate" 9 | ) 10 | 11 | // CRDs contains cluster-api-provider-existinginfra's crds. 12 | var CRDs http.FileSystem = fixeddate.Dir("../../../../../config/crd") 13 | -------------------------------------------------------------------------------- /pkg/utilities/encoding/encoding.go: -------------------------------------------------------------------------------- 1 | package encoding 2 | 3 | import "encoding/base64" 4 | 5 | // This file contains a util to base64 strings for editing secrets 6 | 7 | func Base64Encode(s string) []byte { 8 | strLen := base64.StdEncoding.EncodedLen(len(s)) 9 | sB64 := make([]byte, strLen) 10 | base64.StdEncoding.Encode(sB64, []byte(s)) 11 | return sB64 12 | } 13 | -------------------------------------------------------------------------------- /config/crd/patches/cainjection_in_clusterspecs.yaml: -------------------------------------------------------------------------------- 1 | # The following patch adds a directive for certmanager to inject CA into the CRD 2 | # CRD conversion requires k8s 1.13 or later. 3 | apiVersion: apiextensions.k8s.io/v1beta1 4 | kind: CustomResourceDefinition 5 | metadata: 6 | annotations: 7 | cert-manager.io/inject-ca-from: $(CERTIFICATE_NAMESPACE)/$(CERTIFICATE_NAME) 8 | name: clusterspecs.baremetalproviderspec 9 | -------------------------------------------------------------------------------- /config/crd/patches/cainjection_in_machinespecs.yaml: -------------------------------------------------------------------------------- 1 | # The following patch adds a directive for certmanager to inject CA into the CRD 2 | # CRD conversion requires k8s 1.13 or later. 3 | apiVersion: apiextensions.k8s.io/v1beta1 4 | kind: CustomResourceDefinition 5 | metadata: 6 | annotations: 7 | cert-manager.io/inject-ca-from: $(CERTIFICATE_NAMESPACE)/$(CERTIFICATE_NAME) 8 | name: machinespecs.baremetalproviderspec 9 | -------------------------------------------------------------------------------- /pkg/apis/wksprovider/manifests/yaml/05_sealed_secret_crd.yaml: -------------------------------------------------------------------------------- 1 | --- 2 | apiVersion: apiextensions.k8s.io/v1beta1 3 | kind: CustomResourceDefinition 4 | metadata: 5 | name: sealedsecrets.bitnami.com 6 | spec: 7 | group: bitnami.com 8 | names: 9 | kind: SealedSecret 10 | listKind: SealedSecretList 11 | plural: sealedsecrets 12 | singular: sealedsecret 13 | scope: Namespaced 14 | version: v1alpha1 -------------------------------------------------------------------------------- /config/crd/patches/cainjection_in_existinginfraclusters.yaml: -------------------------------------------------------------------------------- 1 | # The following patch adds a directive for certmanager to inject CA into the CRD 2 | # CRD conversion requires k8s 1.13 or later. 3 | apiVersion: apiextensions.k8s.io/v1beta1 4 | kind: CustomResourceDefinition 5 | metadata: 6 | annotations: 7 | cert-manager.io/inject-ca-from: $(CERTIFICATE_NAMESPACE)/$(CERTIFICATE_NAME) 8 | name: existinginfraclusters.cluster.weave.works 9 | -------------------------------------------------------------------------------- /config/crd/patches/cainjection_in_existinginframachines.yaml: -------------------------------------------------------------------------------- 1 | # The following patch adds a directive for certmanager to inject CA into the CRD 2 | # CRD conversion requires k8s 1.13 or later. 3 | apiVersion: apiextensions.k8s.io/v1beta1 4 | kind: CustomResourceDefinition 5 | metadata: 6 | annotations: 7 | cert-manager.io/inject-ca-from: $(CERTIFICATE_NAMESPACE)/$(CERTIFICATE_NAME) 8 | name: existinginframachines.cluster.weave.works 9 | -------------------------------------------------------------------------------- /pkg/kubernetes/version_test.go: -------------------------------------------------------------------------------- 1 | package kubernetes 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/stretchr/testify/assert" 7 | "github.com/weaveworks/cluster-api-provider-existinginfra/pkg/utilities/version" 8 | ) 9 | 10 | func TestMatchesRangeDefaultVersion(t *testing.T) { 11 | matches, err := version.MatchesRange(DefaultVersion, DefaultVersionsRange) 12 | assert.NoError(t, err) 13 | assert.True(t, matches) 14 | } 15 | -------------------------------------------------------------------------------- /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-met-mon 9 | namespace: system 10 | spec: 11 | endpoints: 12 | - path: /metrics 13 | port: https 14 | selector: 15 | matchLabels: 16 | control-plane: controller-manager 17 | -------------------------------------------------------------------------------- /config/rbac/kustomization.yaml: -------------------------------------------------------------------------------- 1 | resources: 2 | - role.yaml 3 | - role_binding.yaml 4 | - leader_election_role.yaml 5 | - leader_election_role_binding.yaml 6 | # Comment the following 4 lines if you want to disable 7 | # the auth proxy (https://github.com/brancz/kube-rbac-proxy) 8 | # which protects your /metrics endpoint. 9 | - auth_proxy_service.yaml 10 | - auth_proxy_role.yaml 11 | - auth_proxy_role_binding.yaml 12 | - auth_proxy_client_clusterrole.yaml 13 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Binaries for programs and plugins 2 | *.exe 3 | *.exe~ 4 | *.dll 5 | *.so 6 | *.dylib 7 | bin 8 | 9 | # Test binary, build with `go test -c` 10 | *.test 11 | 12 | # Output of the go coverage tool, specifically when used with LiteIDE 13 | *.out 14 | 15 | # Kubernetes Generated files - skip generated files, except for vendored files 16 | 17 | !vendor/**/zz_generated.* 18 | 19 | # editor and IDE paraphernalia 20 | .idea 21 | *.swp 22 | *.swo 23 | *~ 24 | *# -------------------------------------------------------------------------------- /PROJECT: -------------------------------------------------------------------------------- 1 | multigroup: true 2 | repo: github.com/weaveworks/cluster-api-provider-existinginfra 3 | resources: 4 | - group: cluster.weave.works 5 | kind: ExistingInfraCluster 6 | version: v1alpha3 7 | - group: cluster.weave.works 8 | kind: ExistingInfraMachine 9 | version: v1alpha3 10 | - group: baremetalproviderspec 11 | kind: ClusterSpec 12 | version: v1alpha1 13 | - group: baremetalproviderspec 14 | kind: MachineSpec 15 | version: v1alpha1 16 | version: "2" 17 | -------------------------------------------------------------------------------- /config/rbac/clusterspec_viewer_role.yaml: -------------------------------------------------------------------------------- 1 | # permissions for end users to view clusterspecs. 2 | apiVersion: rbac.authorization.k8s.io/v1 3 | kind: ClusterRole 4 | metadata: 5 | name: clusterspec-viewer-role 6 | rules: 7 | - apiGroups: 8 | - baremetalproviderspec 9 | resources: 10 | - clusterspecs 11 | verbs: 12 | - get 13 | - list 14 | - watch 15 | - apiGroups: 16 | - baremetalproviderspec 17 | resources: 18 | - clusterspecs/status 19 | verbs: 20 | - get 21 | -------------------------------------------------------------------------------- /config/rbac/machinespec_viewer_role.yaml: -------------------------------------------------------------------------------- 1 | # permissions for end users to view machinespecs. 2 | apiVersion: rbac.authorization.k8s.io/v1 3 | kind: ClusterRole 4 | metadata: 5 | name: machinespec-viewer-role 6 | rules: 7 | - apiGroups: 8 | - baremetalproviderspec 9 | resources: 10 | - machinespecs 11 | verbs: 12 | - get 13 | - list 14 | - watch 15 | - apiGroups: 16 | - baremetalproviderspec 17 | resources: 18 | - machinespecs/status 19 | verbs: 20 | - get 21 | -------------------------------------------------------------------------------- /config/certmanager/kustomizeconfig.yaml: -------------------------------------------------------------------------------- 1 | # This configuration is for teaching kustomize how to update name ref and var substitution 2 | nameReference: 3 | - kind: Issuer 4 | group: cert-manager.io 5 | fieldSpecs: 6 | - kind: Certificate 7 | group: cert-manager.io 8 | path: spec/issuerRef/name 9 | 10 | varReference: 11 | - kind: Certificate 12 | group: cert-manager.io 13 | path: spec/commonName 14 | - kind: Certificate 15 | group: cert-manager.io 16 | path: spec/dnsNames 17 | -------------------------------------------------------------------------------- /pkg/apis/wksprovider/machine/crds/assets_generate.go: -------------------------------------------------------------------------------- 1 | // +build ignore 2 | 3 | package main 4 | 5 | import ( 6 | "log" 7 | 8 | "github.com/shurcooL/vfsgen" 9 | "github.com/weaveworks/cluster-api-provider-existinginfra/pkg/apis/wksprovider/machine/crds" 10 | ) 11 | 12 | func main() { 13 | err := vfsgen.Generate(crds.CRDs, vfsgen.Options{ 14 | PackageName: "crds", 15 | BuildTags: "!dev", 16 | VariableName: "CRDs", 17 | }) 18 | if err != nil { 19 | log.Fatalln(err) 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /pkg/utilities/path/path.go: -------------------------------------------------------------------------------- 1 | package path 2 | 3 | import ( 4 | "os" 5 | "path/filepath" 6 | "strings" 7 | ) 8 | 9 | // Expand expands the provided path, evaluating all symlinks (including "~"). 10 | func Expand(path string) (string, error) { 11 | path = ExpandHome(path) 12 | return filepath.EvalSymlinks(path) 13 | } 14 | 15 | func ExpandHome(s string) string { 16 | home, _ := os.UserHomeDir() 17 | if strings.HasPrefix(s, "~/") { 18 | return filepath.Join(home, s[2:]) 19 | } 20 | return s 21 | } 22 | -------------------------------------------------------------------------------- /pkg/apis/wksprovider/manifests/assets_generate.go: -------------------------------------------------------------------------------- 1 | // +build ignore 2 | 3 | package main 4 | 5 | import ( 6 | "log" 7 | 8 | "github.com/shurcooL/vfsgen" 9 | "github.com/weaveworks/cluster-api-provider-existinginfra/pkg/apis/wksprovider/manifests" 10 | ) 11 | 12 | func main() { 13 | err := vfsgen.Generate(manifests.Manifests, vfsgen.Options{ 14 | PackageName: "manifests", 15 | BuildTags: "!dev", 16 | VariableName: "Manifests", 17 | }) 18 | if err != nil { 19 | log.Fatalln(err) 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /config/rbac/existinginfracluster_viewer_role.yaml: -------------------------------------------------------------------------------- 1 | # permissions for end users to view existinginfraclusters. 2 | apiVersion: rbac.authorization.k8s.io/v1 3 | kind: ClusterRole 4 | metadata: 5 | name: existinginfracluster-viewer-role 6 | rules: 7 | - apiGroups: 8 | - cluster.weave.works 9 | resources: 10 | - existinginfraclusters 11 | verbs: 12 | - get 13 | - list 14 | - watch 15 | - apiGroups: 16 | - cluster.weave.works 17 | resources: 18 | - existinginfraclusters/status 19 | verbs: 20 | - get 21 | -------------------------------------------------------------------------------- /config/rbac/existinginframachine_viewer_role.yaml: -------------------------------------------------------------------------------- 1 | # permissions for end users to view existinginframachines. 2 | apiVersion: rbac.authorization.k8s.io/v1 3 | kind: ClusterRole 4 | metadata: 5 | name: existinginframachine-viewer-role 6 | rules: 7 | - apiGroups: 8 | - cluster.weave.works 9 | resources: 10 | - existinginframachines 11 | verbs: 12 | - get 13 | - list 14 | - watch 15 | - apiGroups: 16 | - cluster.weave.works 17 | resources: 18 | - existinginframachines/status 19 | verbs: 20 | - get 21 | -------------------------------------------------------------------------------- /pkg/plan/resource/state_test.go: -------------------------------------------------------------------------------- 1 | package resource 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/stretchr/testify/assert" 7 | "github.com/weaveworks/cluster-api-provider-existinginfra/pkg/plan" 8 | ) 9 | 10 | func TestToState(t *testing.T) { 11 | rpm := &RPM{ 12 | Name: "make", 13 | Version: "3.83", 14 | } 15 | expected := plan.State(map[string]interface{}{ 16 | "name": "make", 17 | "version": "3.83", 18 | }) 19 | assert.Equal(t, expected, ToState(rpm)) 20 | assert.Equal(t, expected, rpm.State()) 21 | } 22 | -------------------------------------------------------------------------------- /config/rbac/clusterspec_editor_role.yaml: -------------------------------------------------------------------------------- 1 | # permissions for end users to edit clusterspecs. 2 | apiVersion: rbac.authorization.k8s.io/v1 3 | kind: ClusterRole 4 | metadata: 5 | name: clusterspec-editor-role 6 | rules: 7 | - apiGroups: 8 | - baremetalproviderspec 9 | resources: 10 | - clusterspecs 11 | verbs: 12 | - create 13 | - delete 14 | - get 15 | - list 16 | - patch 17 | - update 18 | - watch 19 | - apiGroups: 20 | - baremetalproviderspec 21 | resources: 22 | - clusterspecs/status 23 | verbs: 24 | - get 25 | -------------------------------------------------------------------------------- /config/rbac/machinespec_editor_role.yaml: -------------------------------------------------------------------------------- 1 | # permissions for end users to edit machinespecs. 2 | apiVersion: rbac.authorization.k8s.io/v1 3 | kind: ClusterRole 4 | metadata: 5 | name: machinespec-editor-role 6 | rules: 7 | - apiGroups: 8 | - baremetalproviderspec 9 | resources: 10 | - machinespecs 11 | verbs: 12 | - create 13 | - delete 14 | - get 15 | - list 16 | - patch 17 | - update 18 | - watch 19 | - apiGroups: 20 | - baremetalproviderspec 21 | resources: 22 | - machinespecs/status 23 | verbs: 24 | - get 25 | -------------------------------------------------------------------------------- /docs/release-process.md: -------------------------------------------------------------------------------- 1 | # Releasing 2 | 3 | We use [GoReleaser](https://goreleaser.com/) to generate release artifacts, which is initiated when a tag pushed to the repository. 4 | 5 | ## Tagging 6 | The tag we use in the repository follows [semver](https://github.com/semver/semver/blob/master/semver.md) with the leading **v**. We want the tag to be signed by the person creating it. 7 | 8 | ``` shell 9 | git checkout 10 | git fetch 11 | git reset --hard origin/ 12 | git tag -s -a vMajor.Minor.patch[-(alpha,beta,rc).#] 13 | git push origin 14 | ``` -------------------------------------------------------------------------------- /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 | group: apiextensions.k8s.io 8 | path: spec/conversion/webhookClientConfig/service/name 9 | 10 | namespace: 11 | - kind: CustomResourceDefinition 12 | group: apiextensions.k8s.io 13 | path: spec/conversion/webhookClientConfig/service/namespace 14 | create: false 15 | 16 | varReference: 17 | - path: metadata/annotations 18 | -------------------------------------------------------------------------------- /pkg/utilities/kubeadm/bootstrap_token_test.go: -------------------------------------------------------------------------------- 1 | package kubeadm_test 2 | 3 | import ( 4 | "regexp" 5 | "testing" 6 | 7 | "github.com/stretchr/testify/assert" 8 | "github.com/weaveworks/cluster-api-provider-existinginfra/pkg/utilities/kubeadm" 9 | ) 10 | 11 | func TestGenerateBootstrapToken(t *testing.T) { 12 | token, err := kubeadm.GenerateBootstrapToken() 13 | assert.NoError(t, err) 14 | assert.NotNil(t, token) 15 | assert.Regexp(t, regexp.MustCompile("^[a-z0-9]{6}$"), token.ID) 16 | assert.Regexp(t, regexp.MustCompile("^[a-z0-9]{16}$"), token.Secret) 17 | } 18 | -------------------------------------------------------------------------------- /config/rbac/existinginfracluster_editor_role.yaml: -------------------------------------------------------------------------------- 1 | # permissions for end users to edit existinginfraclusters. 2 | apiVersion: rbac.authorization.k8s.io/v1 3 | kind: ClusterRole 4 | metadata: 5 | name: existinginfracluster-editor-role 6 | rules: 7 | - apiGroups: 8 | - cluster.weave.works 9 | resources: 10 | - existinginfraclusters 11 | verbs: 12 | - create 13 | - delete 14 | - get 15 | - list 16 | - patch 17 | - update 18 | - watch 19 | - apiGroups: 20 | - cluster.weave.works 21 | resources: 22 | - existinginfraclusters/status 23 | verbs: 24 | - get 25 | -------------------------------------------------------------------------------- /config/rbac/existinginframachine_editor_role.yaml: -------------------------------------------------------------------------------- 1 | # permissions for end users to edit existinginframachines. 2 | apiVersion: rbac.authorization.k8s.io/v1 3 | kind: ClusterRole 4 | metadata: 5 | name: existinginframachine-editor-role 6 | rules: 7 | - apiGroups: 8 | - cluster.weave.works 9 | resources: 10 | - existinginframachines 11 | verbs: 12 | - create 13 | - delete 14 | - get 15 | - list 16 | - patch 17 | - update 18 | - watch 19 | - apiGroups: 20 | - cluster.weave.works 21 | resources: 22 | - existinginframachines/status 23 | verbs: 24 | - get 25 | -------------------------------------------------------------------------------- /golangci.yaml.save: -------------------------------------------------------------------------------- 1 | # https://github.com/marketplace/actions/run-golangci-lint 2 | name: golangci-lint 3 | on: 4 | pull_request: 5 | push: 6 | branches: 7 | - master 8 | jobs: 9 | golangci: 10 | name: lint 11 | runs-on: ubuntu-latest 12 | steps: 13 | - uses: actions/checkout@v2 14 | - name: golangci-lint 15 | uses: golangci/golangci-lint-action@v1 16 | with: 17 | # Required: the version of golangci-lint is required and must be specified without patch version: we always use the latest patch version. 18 | version: v1.30 19 | -------------------------------------------------------------------------------- /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 | - "" 21 | resources: 22 | - configmaps/status 23 | verbs: 24 | - get 25 | - update 26 | - patch 27 | - apiGroups: 28 | - "" 29 | resources: 30 | - events 31 | verbs: 32 | - create 33 | -------------------------------------------------------------------------------- /hack/boilerplate.go.txt: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2020 Weaveworks. 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 | -------------------------------------------------------------------------------- /config/default/manager_webhook_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 | ports: 12 | - containerPort: 9443 13 | name: webhook-server 14 | protocol: TCP 15 | volumeMounts: 16 | - mountPath: /tmp/k8s-webhook-server/serving-certs 17 | name: cert 18 | readOnly: true 19 | volumes: 20 | - name: cert 21 | secret: 22 | defaultMode: 420 23 | secretName: webhook-server-cert 24 | -------------------------------------------------------------------------------- /pkg/plan/runners/sudo/sudo.go: -------------------------------------------------------------------------------- 1 | package sudo 2 | 3 | import ( 4 | "context" 5 | "io" 6 | "strings" 7 | 8 | "github.com/weaveworks/cluster-api-provider-existinginfra/pkg/plan" 9 | ) 10 | 11 | // Runner wraps the inner Runner with sudo. 12 | type Runner struct { 13 | Runner plan.Runner 14 | } 15 | 16 | // RunCommand wraps the command with sudo and passes it on to the wrapped RunCommand. 17 | func (s *Runner) RunCommand(ctx context.Context, cmd string, stdin io.Reader) (stdouterr string, err error) { 18 | return s.Runner.RunCommand(ctx, "sudo -n -- sh -c "+escape(cmd), stdin) 19 | } 20 | 21 | func escape(cmd string) string { 22 | return "'" + strings.ReplaceAll(cmd, "'", "'\"'\"'") + "'" 23 | } 24 | -------------------------------------------------------------------------------- /config/default/webhookcainjection_patch.yaml: -------------------------------------------------------------------------------- 1 | # This patch add annotation to admission webhook config and 2 | # the variables $(CERTIFICATE_NAMESPACE) and $(CERTIFICATE_NAME) will be substituted by kustomize. 3 | apiVersion: admissionregistration.k8s.io/v1beta1 4 | kind: MutatingWebhookConfiguration 5 | metadata: 6 | name: mutating-webhook-configuration 7 | annotations: 8 | cert-manager.io/inject-ca-from: $(CERTIFICATE_NAMESPACE)/$(CERTIFICATE_NAME) 9 | --- 10 | apiVersion: admissionregistration.k8s.io/v1beta1 11 | kind: ValidatingWebhookConfiguration 12 | metadata: 13 | name: validating-webhook-configuration 14 | annotations: 15 | cert-manager.io/inject-ca-from: $(CERTIFICATE_NAMESPACE)/$(CERTIFICATE_NAME) 16 | -------------------------------------------------------------------------------- /config/crd/patches/webhook_in_clusterspecs.yaml: -------------------------------------------------------------------------------- 1 | # The following patch enables conversion webhook for CRD 2 | # CRD conversion requires k8s 1.13 or later. 3 | apiVersion: apiextensions.k8s.io/v1beta1 4 | kind: CustomResourceDefinition 5 | metadata: 6 | name: clusterspecs.baremetalproviderspec 7 | spec: 8 | conversion: 9 | strategy: Webhook 10 | webhookClientConfig: 11 | # this is "\n" used as a placeholder, otherwise it will be rejected by the apiserver for being blank, 12 | # but we're going to set it later using the cert-manager (or potentially a patch if not using cert-manager) 13 | caBundle: Cg== 14 | service: 15 | namespace: system 16 | name: webhook-service 17 | path: /convert 18 | -------------------------------------------------------------------------------- /config/crd/patches/webhook_in_machinespecs.yaml: -------------------------------------------------------------------------------- 1 | # The following patch enables conversion webhook for CRD 2 | # CRD conversion requires k8s 1.13 or later. 3 | apiVersion: apiextensions.k8s.io/v1beta1 4 | kind: CustomResourceDefinition 5 | metadata: 6 | name: machinespecs.baremetalproviderspec 7 | spec: 8 | conversion: 9 | strategy: Webhook 10 | webhookClientConfig: 11 | # this is "\n" used as a placeholder, otherwise it will be rejected by the apiserver for being blank, 12 | # but we're going to set it later using the cert-manager (or potentially a patch if not using cert-manager) 13 | caBundle: Cg== 14 | service: 15 | namespace: system 16 | name: webhook-service 17 | path: /convert 18 | -------------------------------------------------------------------------------- /.golangci.yml: -------------------------------------------------------------------------------- 1 | run: 2 | # timeout for analysis, e.g. 30s, 5m, default is 1m 3 | timeout: 1m 4 | tests: false 5 | 6 | linters: 7 | enable: 8 | # Default 9 | - govet 10 | # - errcheck 11 | # - staticcheck 12 | # - unused 13 | # - gosimple 14 | # - structcheck 15 | # - varcheck 16 | # - ineffassign 17 | # - deadcode 18 | # - typecheck 19 | # # Extra, see list of https://golangci-lint.run/usage/linters/ 20 | # - bodyclose 21 | # - unconvert 22 | # - dupl 23 | # - goconst 24 | # - gocyclo 25 | # - asciicheck 26 | # - gofmt 27 | # - goimports 28 | # - misspell 29 | # - dogsled 30 | # - nakedret 31 | # - prealloc 32 | # - gocritic 33 | # - goprintffuncname 34 | # - exhaustive 35 | # - nolintlint 36 | -------------------------------------------------------------------------------- /config/crd/patches/webhook_in_existinginfraclusters.yaml: -------------------------------------------------------------------------------- 1 | # The following patch enables conversion webhook for CRD 2 | # CRD conversion requires k8s 1.13 or later. 3 | apiVersion: apiextensions.k8s.io/v1beta1 4 | kind: CustomResourceDefinition 5 | metadata: 6 | name: existinginfraclusters.cluster.weave.works 7 | spec: 8 | conversion: 9 | strategy: Webhook 10 | webhookClientConfig: 11 | # this is "\n" used as a placeholder, otherwise it will be rejected by the apiserver for being blank, 12 | # but we're going to set it later using the cert-manager (or potentially a patch if not using cert-manager) 13 | caBundle: Cg== 14 | service: 15 | namespace: system 16 | name: webhook-service 17 | path: /convert 18 | -------------------------------------------------------------------------------- /config/crd/patches/webhook_in_existinginframachines.yaml: -------------------------------------------------------------------------------- 1 | # The following patch enables conversion webhook for CRD 2 | # CRD conversion requires k8s 1.13 or later. 3 | apiVersion: apiextensions.k8s.io/v1beta1 4 | kind: CustomResourceDefinition 5 | metadata: 6 | name: existinginframachines.cluster.weave.works 7 | spec: 8 | conversion: 9 | strategy: Webhook 10 | webhookClientConfig: 11 | # this is "\n" used as a placeholder, otherwise it will be rejected by the apiserver for being blank, 12 | # but we're going to set it later using the cert-manager (or potentially a patch if not using cert-manager) 13 | caBundle: Cg== 14 | service: 15 | namespace: system 16 | name: webhook-service 17 | path: /convert 18 | -------------------------------------------------------------------------------- /test/plan/testutils/mock_test.go: -------------------------------------------------------------------------------- 1 | package testutils 2 | 3 | import ( 4 | "context" 5 | "testing" 6 | 7 | "github.com/pkg/errors" 8 | "github.com/stretchr/testify/assert" 9 | ) 10 | 11 | func TestSetRunCommandNoError(t *testing.T) { 12 | r := &MockRunner{} 13 | r.SetRunCommand("foo", nil) 14 | out, err := r.RunCommand(context.Background(), "bar", nil) 15 | assert.Equal(t, "foo", out) 16 | assert.NoError(t, err) 17 | } 18 | 19 | func TestSetRunCommandWithError(t *testing.T) { 20 | r := &MockRunner{} 21 | errstr := "error running command bar" 22 | r.SetRunCommand("", errors.New(errstr)) 23 | out, err := r.RunCommand(context.Background(), "bar", nil) 24 | assert.Empty(t, out) 25 | assert.Error(t, err) 26 | assert.EqualError(t, err, errstr) 27 | } 28 | -------------------------------------------------------------------------------- /hack/image-tag: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | set -o errexit 4 | set -o nounset 5 | set -o pipefail 6 | 7 | WORKING_SUFFIX=$(if git status --porcelain | grep -qE '^(?:[^?][^ ]|[^ ][^?])\s'; then echo "-WIP"; else echo ""; fi) 8 | CURRENT_TAG=$(if TAG=$(git describe --tags --exact-match 2>/dev/null); then echo $TAG; else echo ""; fi) 9 | if test -z "$WORKING_SUFFIX" && test ! -z "$CURRENT_TAG" 10 | then 11 | echo $CURRENT_TAG 12 | else 13 | BRANCH_PREFIX=$(git rev-parse --abbrev-ref HEAD) 14 | 15 | # Fix the object name prefix length to 8 characters to have it consistent across the system. 16 | # See https://git-scm.com/docs/git-rev-parse#Documentation/git-rev-parse.txt---shortlength 17 | echo "${BRANCH_PREFIX//\//-}-$(git rev-parse --short=8 HEAD)$WORKING_SUFFIX" 18 | fi 19 | -------------------------------------------------------------------------------- /tools/image-tag: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | set -o errexit 4 | set -o nounset 5 | set -o pipefail 6 | 7 | WORKING_SUFFIX=$(if git status --porcelain | grep -qE '^(?:[^?][^ ]|[^ ][^?])\s'; then echo "-WIP"; else echo ""; fi) 8 | CURRENT_TAG=$(if TAG=$(git describe --tags --exact-match 2>/dev/null); then echo $TAG; else echo ""; fi) 9 | if test -z "$WORKING_SUFFIX" && test ! -z "$CURRENT_TAG" 10 | then 11 | echo $CURRENT_TAG 12 | else 13 | BRANCH_PREFIX=$(git rev-parse --abbrev-ref HEAD) 14 | 15 | # Fix the object name prefix length to 8 characters to have it consistent across the system. 16 | # See https://git-scm.com/docs/git-rev-parse#Documentation/git-rev-parse.txt---shortlength 17 | echo "${BRANCH_PREFIX//\//-}-$(git rev-parse --short=8 HEAD)$WORKING_SUFFIX" 18 | fi 19 | -------------------------------------------------------------------------------- /pkg/apis/wksprovider/machine/config/kubeproxy/kubeproxy.go: -------------------------------------------------------------------------------- 1 | package kubeproxy 2 | 3 | import ( 4 | metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" 5 | kubeproxycfg "k8s.io/kube-proxy/config/v1alpha1" 6 | "k8s.io/utils/pointer" 7 | ) 8 | 9 | const kubeProxyConfiguration = "KubeProxyConfiguration" 10 | 11 | // NewConfig returns an KubeProxyConfiguration with appropriate 12 | // defaults set for WKS. 13 | func NewConfig(conntrackMax int32) *kubeproxycfg.KubeProxyConfiguration { 14 | return &kubeproxycfg.KubeProxyConfiguration{ 15 | TypeMeta: metav1.TypeMeta{ 16 | Kind: kubeProxyConfiguration, 17 | APIVersion: kubeproxycfg.SchemeGroupVersion.String(), 18 | }, 19 | Conntrack: kubeproxycfg.KubeProxyConntrackConfiguration{ 20 | MaxPerCore: pointer.Int32Ptr(conntrackMax), 21 | }, 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /apis/baremetalproviderspec/v1alpha1/doc.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2020 Weaveworks. 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 | // +k8s:conversion-gen=github.com/weaveworks/cluster-api-provider-existinginfra/apis/cluster.weave.works/v1alpha3 18 | // +kubebuilder:skip 19 | package v1alpha1 20 | -------------------------------------------------------------------------------- /pkg/plan/runner_local.go: -------------------------------------------------------------------------------- 1 | package plan 2 | 3 | import ( 4 | "context" 5 | "io" 6 | "os/exec" 7 | "syscall" 8 | ) 9 | 10 | // LocalRunner is a runner executing commands on the same host it's running on. 11 | type LocalRunner struct{} 12 | 13 | var _ Runner = &LocalRunner{} 14 | 15 | // RunCommand implements Runner. 16 | func (r *LocalRunner) RunCommand(ctx context.Context, cmd string, stdin io.Reader) (stdouterr string, err error) { 17 | command := exec.Command("sh", "-c", cmd) 18 | command.Stdin = stdin 19 | 20 | output, err := command.CombinedOutput() 21 | return string(output), extractExitCode(err) 22 | } 23 | 24 | func extractExitCode(err error) error { 25 | if err, ok := err.(*exec.ExitError); ok { 26 | if stat, ok := err.Sys().(syscall.WaitStatus); ok { 27 | return &RunError{ExitCode: stat.ExitStatus()} 28 | } 29 | } 30 | return err 31 | } 32 | -------------------------------------------------------------------------------- /pkg/plan/resource/plan_serialization_test.go: -------------------------------------------------------------------------------- 1 | package resource 2 | 3 | import ( 4 | "strings" 5 | "testing" 6 | 7 | "github.com/stretchr/testify/assert" 8 | "github.com/weaveworks/cluster-api-provider-existinginfra/pkg/plan" 9 | ) 10 | 11 | // Tests real Resources in the main 'resource' package 12 | func TestPlanToJSON(t *testing.T) { 13 | b := plan.NewBuilder() 14 | b.AddResource("rpm:foo", &RPM{Name: "foo", Version: "2"}) 15 | b.AddResource("service:bar", &Service{Name: "bar", Status: "OK", Enabled: true}, plan.DependOn("rpm:foo")) 16 | b.AddResource("file:baz", &File{Source: "/tmp/x", Destination: "/etc/y"}, plan.DependOn("service:bar", "rpm:foo")) 17 | 18 | pin, err := b.Plan() 19 | assert.NoError(t, err) 20 | pout, err := plan.NewPlanFromJSON(strings.NewReader(pin.ToJSON())) 21 | assert.NoError(t, err) 22 | assert.True(t, plan.EqualPlans(pin, pout)) 23 | } 24 | -------------------------------------------------------------------------------- /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.5.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=10" 19 | ports: 20 | - containerPort: 8443 21 | name: https 22 | - name: manager 23 | args: 24 | - "--metrics-addr=127.0.0.1:8080" 25 | - "--enable-leader-election" 26 | -------------------------------------------------------------------------------- /pkg/plan/resource/parsing.go: -------------------------------------------------------------------------------- 1 | package resource 2 | 3 | import ( 4 | "bufio" 5 | "strings" 6 | ) 7 | 8 | // A few utility function to parse program outputs 9 | 10 | // line return the first line of output. 11 | func line(output string) string { 12 | stringReader := strings.NewReader(output) 13 | r := bufio.NewReader(stringReader) 14 | l, err := r.ReadString('\n') 15 | if err != nil { 16 | return output 17 | } 18 | return strings.TrimRight(l, "\n") 19 | } 20 | 21 | // keyval parses key=val lines and return the val for the corresponding key. 22 | func keyval(output, key string) string { 23 | keyequal := key + "=" 24 | 25 | scanner := bufio.NewScanner(strings.NewReader(output)) 26 | for scanner.Scan() { 27 | line := scanner.Text() 28 | if strings.HasPrefix(line, keyequal) { 29 | return strings.Trim(line[len(keyequal):], "\"") 30 | } 31 | } 32 | 33 | return "" 34 | } 35 | -------------------------------------------------------------------------------- /config/webhook/kustomizeconfig.yaml: -------------------------------------------------------------------------------- 1 | # the following config is for teaching kustomize where to look at when substituting vars. 2 | # It requires kustomize v2.1.0 or newer to work properly. 3 | nameReference: 4 | - kind: Service 5 | version: v1 6 | fieldSpecs: 7 | - kind: MutatingWebhookConfiguration 8 | group: admissionregistration.k8s.io 9 | path: webhooks/clientConfig/service/name 10 | - kind: ValidatingWebhookConfiguration 11 | group: admissionregistration.k8s.io 12 | path: webhooks/clientConfig/service/name 13 | 14 | namespace: 15 | - kind: MutatingWebhookConfiguration 16 | group: admissionregistration.k8s.io 17 | path: webhooks/clientConfig/service/namespace 18 | create: true 19 | - kind: ValidatingWebhookConfiguration 20 | group: admissionregistration.k8s.io 21 | path: webhooks/clientConfig/service/namespace 22 | create: true 23 | 24 | varReference: 25 | - path: metadata/annotations 26 | -------------------------------------------------------------------------------- /pkg/utilities/version/version_test.go: -------------------------------------------------------------------------------- 1 | package version_test 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/stretchr/testify/assert" 7 | "github.com/weaveworks/cluster-api-provider-existinginfra/pkg/utilities/version" 8 | ) 9 | 10 | func TestVersionLessthanWithBothVs(t *testing.T) { 11 | lt, err := version.LessThan("v1.14.7", "v1.15.0") 12 | assert.NoError(t, err) 13 | assert.True(t, lt) 14 | } 15 | 16 | func TestVersionLessthanWithFormerV(t *testing.T) { 17 | lt, err := version.LessThan("v1.14.7", "1.15.0") 18 | assert.NoError(t, err) 19 | assert.True(t, lt) 20 | } 21 | 22 | func TestVersionLessthanWithLatterV(t *testing.T) { 23 | lt, err := version.LessThan("1.14.7", "v1.15.0") 24 | assert.NoError(t, err) 25 | assert.True(t, lt) 26 | } 27 | 28 | func TestVersionLessthanWithOutV(t *testing.T) { 29 | lt, err := version.LessThan("1.14.7", "1.15.0") 30 | assert.NoError(t, err) 31 | assert.True(t, lt) 32 | } 33 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | # Build the manager binary 2 | FROM golang:1.14 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 apis/ apis/ 15 | COPY controllers/ controllers/ 16 | COPY pkg/ pkg/ 17 | 18 | # Build 19 | RUN CGO_ENABLED=0 GOOS=linux GOARCH=amd64 GO111MODULE=on go build -a -o manager main.go 20 | 21 | # Use distroless as minimal base image to package the manager binary 22 | # Refer to https://github.com/GoogleContainerTools/distroless for more details 23 | FROM gcr.io/distroless/static:nonroot 24 | WORKDIR / 25 | COPY --from=builder /workspace/manager . 26 | USER nonroot:nonroot 27 | 28 | ENTRYPOINT ["/manager"] 29 | -------------------------------------------------------------------------------- /pkg/apis/wksprovider/machine/scripts/run.go: -------------------------------------------------------------------------------- 1 | package scripts 2 | 3 | import ( 4 | "bytes" 5 | "context" 6 | "fmt" 7 | "io" 8 | "os" 9 | ) 10 | 11 | // runner is something that can run a command somewhere. 12 | // 13 | // N.B.: this interface is meant to match pkg/plan.Runner, and is used in order 14 | // to decouple packages. 15 | type runner interface { 16 | // RunCommand runs the provided command in a shell. 17 | // cmd can be more than one single command, it can be a full shell script. 18 | RunCommand(ctx context.Context, cmd string, stdin io.Reader) (stdouterr string, err error) 19 | } 20 | 21 | func WriteFile(ctx context.Context, content []byte, dstPath string, perm os.FileMode, runner runner) error { 22 | input := bytes.NewReader(content) 23 | cmd := fmt.Sprintf("mkdir -pv $(dirname %q) && sed -n 'w %s' && chmod 0%o %q", dstPath, dstPath, perm, dstPath) 24 | _, err := runner.RunCommand(ctx, cmd, input) 25 | return err 26 | } 27 | -------------------------------------------------------------------------------- /test/plan/testutils/runner_mock.go: -------------------------------------------------------------------------------- 1 | package testutils 2 | 3 | import ( 4 | "context" 5 | "io" 6 | ) 7 | 8 | // MockRunner needed for testing plans 9 | type MockRunner struct { 10 | Output string 11 | Err error 12 | } 13 | 14 | func (r *MockRunner) clearRunnerState() { 15 | r.Output = "" 16 | r.Err = nil 17 | } 18 | 19 | func (r *MockRunner) setRunnerState(out string, err error) { 20 | r.Output = out 21 | r.Err = err 22 | } 23 | 24 | // SetRunCommand allows you to configure the output for RunCommand 25 | func (r *MockRunner) SetRunCommand(out string, err error) { 26 | r.setRunnerState(out, err) 27 | } 28 | 29 | // ClearRunCommand undoes any output configured by SetRunCommand 30 | func (r *MockRunner) ClearRunCommand() { 31 | r.clearRunnerState() 32 | } 33 | 34 | // RunCommand returns the test Output and Err values 35 | func (r *MockRunner) RunCommand(_ context.Context, _ string, _ io.Reader) (string, error) { 36 | return r.Output, r.Err 37 | } 38 | -------------------------------------------------------------------------------- /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 | labels: 23 | control-plane: controller-manager 24 | spec: 25 | containers: 26 | - command: 27 | - /manager 28 | args: 29 | - --enable-leader-election 30 | image: controller:latest 31 | name: manager 32 | resources: 33 | limits: 34 | cpu: 100m 35 | memory: 30Mi 36 | requests: 37 | cpu: 100m 38 | memory: 20Mi 39 | terminationGracePeriodSeconds: 10 40 | -------------------------------------------------------------------------------- /pkg/apis/wksprovider/manifests/yaml/04_capi_controller.yaml: -------------------------------------------------------------------------------- 1 | --- 2 | apiVersion: apps/v1 3 | kind: Deployment 4 | metadata: 5 | name: capi-controller 6 | namespace: system 7 | labels: 8 | name: capi-controller 9 | spec: 10 | replicas: 1 11 | selector: 12 | matchLabels: 13 | name: capi-controller 14 | template: 15 | metadata: 16 | labels: 17 | name: capi-controller 18 | spec: 19 | tolerations: 20 | # Allow scheduling on master nodes; required during bootstrapping. 21 | - effect: NoSchedule 22 | key: node-role.kubernetes.io/master 23 | operator: Exists 24 | # Mark this as a critical addon: 25 | - key: CriticalAddonsOnly 26 | operator: Exists 27 | containers: 28 | - name: controller 29 | image: us.gcr.io/k8s-artifacts-prod/cluster-api/cluster-api-controller:v0.3.5 30 | resources: 31 | requests: 32 | cpu: 100m 33 | memory: 20Mi 34 | -------------------------------------------------------------------------------- /pkg/plan/resource/base.go: -------------------------------------------------------------------------------- 1 | package resource 2 | 3 | import ( 4 | "context" 5 | 6 | "github.com/weaveworks/cluster-api-provider-existinginfra/pkg/plan" 7 | ) 8 | 9 | // Base can be embedded into a struct to provide a default implementation of 10 | // plan.Resource. 11 | type Base struct{} 12 | 13 | var _ plan.Resource = plan.RegisterResource(&Base{}) 14 | 15 | // State implements plan.Resource. 16 | func (b *Base) State() plan.State { 17 | return plan.EmptyState 18 | } 19 | 20 | // QueryState implements plan.Resource. 21 | func (b *Base) QueryState(ctx context.Context, runner plan.Runner) (plan.State, error) { 22 | return plan.EmptyState, nil 23 | } 24 | 25 | // Apply implements plan.Resource. 26 | func (b *Base) Apply(ctx context.Context, runner plan.Runner, diff plan.Diff) (bool, error) { 27 | return true, nil 28 | } 29 | 30 | // Undo implements plan.Resource. 31 | func (b *Base) Undo(ctx context.Context, runner plan.Runner, current plan.State) error { 32 | return nil 33 | } 34 | -------------------------------------------------------------------------------- /pkg/specs/specs_test.go: -------------------------------------------------------------------------------- 1 | package specs 2 | 3 | import ( 4 | "io/ioutil" 5 | "testing" 6 | 7 | "github.com/tj/assert" 8 | "github.com/weaveworks/cluster-api-provider-existinginfra/apis/cluster.weave.works/v1alpha3" 9 | clusterv1 "sigs.k8s.io/cluster-api/api/v1alpha3" 10 | ) 11 | 12 | func TestClusterManifestHandling(t *testing.T) { 13 | name := "foo" 14 | c := clusterv1.Cluster{} 15 | eic := v1alpha3.ExistingInfraCluster{} 16 | 17 | cfile, err := ioutil.TempFile("", "") 18 | assert.NoError(t, err) 19 | c.ObjectMeta.Name = name 20 | c.APIVersion = "cluster.x-k8s.io/v1alpha3" 21 | c.Kind = "Cluster" 22 | 23 | eic.ObjectMeta.Name = name 24 | eic.APIVersion = "cluster.weave.works/v1alpha3" 25 | eic.Kind = "ExistingInfraCluster" 26 | err = WriteManifest(&c, &eic, cfile.Name()) 27 | assert.NoError(t, err) 28 | c2, eic2, err := ParseClusterManifest(cfile.Name()) 29 | assert.NoError(t, err) 30 | assert.Equal(t, c.ObjectMeta.Name, c2.ObjectMeta.Name) 31 | assert.Equal(t, eic.ObjectMeta.Name, eic2.ObjectMeta.Name) 32 | } 33 | -------------------------------------------------------------------------------- /.github/workflows/build.yaml: -------------------------------------------------------------------------------- 1 | name: build 2 | 3 | on: 4 | pull_request: 5 | push: 6 | branches: 7 | - master 8 | 9 | jobs: 10 | test: 11 | runs-on: ubuntu-latest 12 | steps: 13 | - name: Checkout 14 | uses: actions/checkout@v2 15 | - name: Restore Go cache 16 | uses: actions/cache@v1 17 | with: 18 | path: ~/go/pkg/mod 19 | key: ${{ runner.os }}-go-${{ hashFiles('**/go.sum') }} 20 | restore-keys: | 21 | ${{ runner.os }}-go- 22 | - name: Setup Go 23 | uses: actions/setup-go@v2 24 | with: 25 | go-version: 1.14.x 26 | - name: Run tests 27 | run: make test 28 | - name: Upload coverage to Codecov 29 | uses: codecov/codecov-action@v1 30 | with: 31 | file: ./cover.out 32 | - name: Check if working tree is dirty 33 | run: | 34 | if [[ $(git diff --stat) != '' ]]; then 35 | echo 'run make test and commit changes' 36 | exit 1 37 | fi 38 | -------------------------------------------------------------------------------- /config/certmanager/certificate.yaml: -------------------------------------------------------------------------------- 1 | # The following manifests contain a self-signed issuer CR and a certificate CR. 2 | # More document can be found at https://docs.cert-manager.io 3 | # WARNING: Targets CertManager 0.11 check https://docs.cert-manager.io/en/latest/tasks/upgrading/index.html for 4 | # breaking changes 5 | apiVersion: cert-manager.io/v1alpha2 6 | kind: Issuer 7 | metadata: 8 | name: selfsigned-issuer 9 | namespace: system 10 | spec: 11 | selfSigned: {} 12 | --- 13 | apiVersion: cert-manager.io/v1alpha2 14 | kind: Certificate 15 | metadata: 16 | name: serving-cert # this name should match the one appeared in kustomizeconfig.yaml 17 | namespace: system 18 | spec: 19 | # $(SERVICE_NAME) and $(SERVICE_NAMESPACE) will be substituted by kustomize 20 | dnsNames: 21 | - $(SERVICE_NAME).$(SERVICE_NAMESPACE).svc 22 | - $(SERVICE_NAME).$(SERVICE_NAMESPACE).svc.cluster.local 23 | issuerRef: 24 | kind: Issuer 25 | name: selfsigned-issuer 26 | secretName: webhook-server-cert # this secret will not be prefixed, since it's not managed by kustomize 27 | -------------------------------------------------------------------------------- /pkg/plan/resource/run_test.go: -------------------------------------------------------------------------------- 1 | package resource 2 | 3 | import ( 4 | "context" 5 | "io/ioutil" 6 | "os" 7 | "path/filepath" 8 | "testing" 9 | 10 | "github.com/stretchr/testify/assert" 11 | "github.com/weaveworks/cluster-api-provider-existinginfra/pkg/plan" 12 | "github.com/weaveworks/cluster-api-provider-existinginfra/pkg/utilities/object" 13 | ) 14 | 15 | func TestRunAndUndo(t *testing.T) { 16 | ctx := context.Background() 17 | dir, err := ioutil.TempDir("", "run-test") 18 | assert.NoError(t, err) 19 | filename := filepath.Join(dir, "foo") 20 | res := &Run{ 21 | Script: object.String("touch " + filename), 22 | UndoScript: object.String("rm " + filename), 23 | } 24 | 25 | runner := &plan.LocalRunner{} 26 | _, err = os.Stat(filename) 27 | assert.True(t, os.IsNotExist(err)) 28 | 29 | val, err := res.Apply(ctx, runner, plan.EmptyDiff()) 30 | assert.True(t, val) 31 | assert.NoError(t, err) 32 | assert.FileExists(t, filename) 33 | 34 | err = res.Undo(ctx, runner, plan.EmptyState) 35 | assert.NoError(t, err) 36 | _, err = os.Stat(filename) 37 | assert.True(t, os.IsNotExist(err)) 38 | } 39 | -------------------------------------------------------------------------------- /pkg/utilities/fixeddate/fixeddate.go: -------------------------------------------------------------------------------- 1 | // Package fixeddate implements a http.FileSystem that gives each file a fixed date. 2 | // This is used with the vfsdata generator to avoid spurious diffs. 3 | package fixeddate 4 | 5 | import ( 6 | "net/http" 7 | "os" 8 | "time" 9 | ) 10 | 11 | // Dir is a wrapper around http.Dir that gives every file a fixed date. 12 | type Dir string 13 | 14 | // Open implements http.FileSystem 15 | func (f Dir) Open(name string) (http.File, error) { 16 | dir, err := http.Dir(f).Open(name) 17 | return fixedDateFile{File: dir}, err 18 | } 19 | 20 | type fixedDateFile struct { 21 | http.File 22 | } 23 | 24 | // Stat overrides the same method in http.File 25 | func (f fixedDateFile) Stat() (os.FileInfo, error) { 26 | info, err := f.File.Stat() 27 | return fixedDateFileInfo{FileInfo: info}, err 28 | } 29 | 30 | type fixedDateFileInfo struct { 31 | os.FileInfo 32 | } 33 | 34 | var fixedDate, _ = time.Parse(time.RFC3339, "2020-01-01T00:00:00Z") 35 | 36 | // ModTime overrides the same method in os.FileInfo 37 | func (f fixedDateFileInfo) ModTime() time.Time { 38 | return fixedDate 39 | } 40 | -------------------------------------------------------------------------------- /pkg/cluster/machine/machine_test.go: -------------------------------------------------------------------------------- 1 | package machine 2 | 3 | import ( 4 | "io/ioutil" 5 | "testing" 6 | 7 | "github.com/tj/assert" 8 | "github.com/weaveworks/cluster-api-provider-existinginfra/apis/cluster.weave.works/v1alpha3" 9 | clusterv1 "sigs.k8s.io/cluster-api/api/v1alpha3" 10 | ) 11 | 12 | func TestMachineManifestHandling(t *testing.T) { 13 | name := "foo" 14 | m := clusterv1.Machine{} 15 | eim := v1alpha3.ExistingInfraMachine{} 16 | 17 | cfile, err := ioutil.TempFile("", "") 18 | assert.NoError(t, err) 19 | m.ObjectMeta.Name = name 20 | m.APIVersion = "cluster.x-k8s.io/v1alpha3" 21 | m.Kind = "Machine" 22 | 23 | eim.ObjectMeta.Name = name 24 | eim.APIVersion = "cluster.weave.works/v1alpha3" 25 | eim.Kind = "ExistingInfraMachine" 26 | ms := []*clusterv1.Machine{&m} 27 | eims := []*v1alpha3.ExistingInfraMachine{&eim} 28 | 29 | err = WriteManifest(ms, eims, cfile.Name()) 30 | assert.NoError(t, err) 31 | m2, eim2, err := ParseManifest(cfile.Name()) 32 | assert.NoError(t, err) 33 | assert.Equal(t, m.ObjectMeta.Name, m2[0].ObjectMeta.Name) 34 | assert.Equal(t, eim.ObjectMeta.Name, eim2[0].ObjectMeta.Name) 35 | } 36 | -------------------------------------------------------------------------------- /pkg/plan/resource/parsing_test.go: -------------------------------------------------------------------------------- 1 | package resource 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/stretchr/testify/assert" 7 | ) 8 | 9 | func TestLine(t *testing.T) { 10 | tests := []struct { 11 | output, expected string 12 | }{ 13 | {"foo", "foo"}, 14 | {"foo\n", "foo"}, 15 | {"foo\nbar\n", "foo"}, 16 | } 17 | 18 | for _, test := range tests { 19 | assert.Equal(t, test.expected, line(test.output)) 20 | } 21 | } 22 | 23 | const testSystemdShow = ` 24 | Delegate=no 25 | CPUAccounting=no 26 | CPUShares=18446744073709551615 27 | StartupCPUShares=18446744073709551615 28 | CPUQuotaPerSecUSec=infinity 29 | BlockIOAccounting=no 30 | BlockIOWeight=18446744073709551615 31 | StartupBlockIOWeight=18446744073709551615 32 | MemoryAccounting=no 33 | MemoryLimit=18446744073709551615 34 | DevicePolicy=auto 35 | ` 36 | 37 | func TestKeyval(t *testing.T) { 38 | tests := []struct { 39 | output, key, expected string 40 | }{ 41 | {"foo=bar", "foo", "bar"}, 42 | {"foo=\"bar\"", "foo", "bar"}, 43 | {"foo=bar", "meh", ""}, 44 | {testSystemdShow, "MemoryAccounting", "no"}, 45 | } 46 | 47 | for _, test := range tests { 48 | v := keyval(test.output, test.key) 49 | assert.Equal(t, test.expected, v) 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /pkg/plan/runner_local_test.go: -------------------------------------------------------------------------------- 1 | package plan 2 | 3 | import ( 4 | "context" 5 | "regexp" 6 | "testing" 7 | 8 | "github.com/stretchr/testify/assert" 9 | ) 10 | 11 | func TestLocalRunnerRunCommand(t *testing.T) { 12 | var r Runner = &LocalRunner{} 13 | output, err := r.RunCommand(context.Background(), `echo "success"`, nil) 14 | assert.NoError(t, err) 15 | assert.Equal(t, "success\n", output) 16 | } 17 | 18 | func TestLocalRunnerCommandNotFound(t *testing.T) { 19 | var r Runner = &LocalRunner{} 20 | output, err := r.RunCommand(context.Background(), `foofoo`, nil) 21 | assert.Error(t, err) 22 | assert.Regexp(t, regexp.MustCompile("foofoo:.*not found"), output) 23 | } 24 | 25 | func TestLocalRunnerExitCode(t *testing.T) { 26 | for _, tt := range []struct { 27 | command string 28 | wantExitCode int 29 | }{ 30 | {"(exit 0)", 0}, 31 | {"(exit 1)", 1}, 32 | {"(exit 2)", 2}, 33 | {"(exit 58)", 58}, 34 | } { 35 | wantError := (tt.wantExitCode != 0) 36 | 37 | t.Run(tt.command, func(t *testing.T) { 38 | var r Runner = &LocalRunner{} 39 | _, gotErr := r.RunCommand(context.Background(), tt.command, nil) 40 | 41 | assert.Equal(t, wantError, gotErr != nil) 42 | 43 | if wantError { 44 | assert.Equal(t, tt.wantExitCode, gotErr.(*RunError).ExitCode) 45 | } 46 | }) 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /.github/workflows/goreleaser.yaml: -------------------------------------------------------------------------------- 1 | name: goreleaser 2 | 3 | on: 4 | push: 5 | tags: 6 | - '*' 7 | 8 | jobs: 9 | goreleaser: 10 | runs-on: ubuntu-latest 11 | steps: 12 | - name: Checkout 13 | uses: actions/checkout@v2 14 | - name: Unshallow 15 | run: git fetch --prune --unshallow 16 | - name: Set up Go 17 | uses: actions/setup-go@v2 18 | with: 19 | go-version: 1.14 20 | # Courtesy of https://github.com/fluxcd/toolkit/blob/master/.github/workflows/release.yaml 21 | - name: Download release notes utility 22 | env: 23 | GH_REL_URL: https://github.com/buchanae/github-release-notes/releases/download/0.2.0/github-release-notes-linux-amd64-0.2.0.tar.gz 24 | run: cd /tmp && curl -sSL ${GH_REL_URL} | tar xz && sudo mv github-release-notes /usr/local/bin/ 25 | - name: Generate release notes 26 | run: | 27 | echo 'CHANGELOG' > /tmp/release.txt 28 | github-release-notes -org weaveworks -repo cluster-api-provider-existinginfra -since-latest-release -include-author >> /tmp/release.txt 29 | - name: Run GoReleaser 30 | uses: goreleaser/goreleaser-action@v2 31 | with: 32 | version: latest 33 | args: release --release-notes=/tmp/release.txt --rm-dist 34 | env: 35 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 36 | -------------------------------------------------------------------------------- /config/rbac/role.yaml: -------------------------------------------------------------------------------- 1 | 2 | --- 3 | apiVersion: rbac.authorization.k8s.io/v1 4 | kind: ClusterRole 5 | metadata: 6 | creationTimestamp: null 7 | name: manager-role 8 | rules: 9 | - apiGroups: 10 | - "" 11 | resources: 12 | - configmaps 13 | verbs: 14 | - get 15 | - list 16 | - patch 17 | - apiGroups: 18 | - cluster.weave.works 19 | resources: 20 | - existinginfrabootstraps 21 | verbs: 22 | - create 23 | - delete 24 | - get 25 | - list 26 | - patch 27 | - update 28 | - watch 29 | - apiGroups: 30 | - cluster.weave.works 31 | resources: 32 | - existinginfrabootstraps/status 33 | verbs: 34 | - get 35 | - patch 36 | - update 37 | - apiGroups: 38 | - cluster.weave.works 39 | resources: 40 | - existinginfraclusters 41 | verbs: 42 | - create 43 | - delete 44 | - get 45 | - list 46 | - patch 47 | - update 48 | - watch 49 | - apiGroups: 50 | - cluster.weave.works 51 | resources: 52 | - existinginfraclusters/status 53 | verbs: 54 | - get 55 | - patch 56 | - update 57 | - apiGroups: 58 | - cluster.weave.works 59 | resources: 60 | - existinginframachines 61 | verbs: 62 | - create 63 | - delete 64 | - get 65 | - list 66 | - patch 67 | - update 68 | - watch 69 | - apiGroups: 70 | - cluster.weave.works 71 | resources: 72 | - existinginframachines/status 73 | verbs: 74 | - get 75 | - patch 76 | - update 77 | -------------------------------------------------------------------------------- /pkg/plan/recipe/renew_kubeadm_certs.go: -------------------------------------------------------------------------------- 1 | package recipe 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | "strings" 7 | 8 | "github.com/weaveworks/cluster-api-provider-existinginfra/pkg/plan" 9 | "github.com/weaveworks/cluster-api-provider-existinginfra/pkg/plan/resource" 10 | "github.com/weaveworks/cluster-api-provider-existinginfra/pkg/utilities/object" 11 | ) 12 | 13 | func BuildGetKubeadmCertKeyPlan(ctx context.Context, certificateKey *string) (*plan.Plan, error) { 14 | b := plan.NewBuilder() 15 | 16 | b.AddResource( 17 | "renew-certs:kubeadm-alpha-certs-cert-key", 18 | &resource.Run{ 19 | Script: object.String("kubeadm alpha certs certificate-key"), 20 | Output: certificateKey}, 21 | ) 22 | *certificateKey = strings.TrimSuffix(*certificateKey, "\n") 23 | 24 | p, err := b.Plan() 25 | if err != nil { 26 | return nil, err 27 | } 28 | return &p, nil 29 | } 30 | 31 | func BuildUploadKubeadmCertsPlan(ctx context.Context, certificateKey string) (*plan.Plan, error) { 32 | b := plan.NewBuilder() 33 | 34 | // run kubeadm init phase upload-certs --upload-certs and certificate key output to env var 35 | b.AddResource( 36 | "renew-certs:kubeadm-upload-certs", 37 | &resource.Run{Script: object.String(fmt.Sprintf("kubeadm init phase upload-certs --upload-certs --certificate-key=%s > /dev/null", certificateKey))}) 38 | 39 | p, err := b.Plan() 40 | if err != nil { 41 | return nil, err 42 | } 43 | return &p, nil 44 | } 45 | -------------------------------------------------------------------------------- /apis/cluster.weave.works/v1alpha3/groupversion_info.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2020 Weaveworks. 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 v1alpha3 contains API Schema definitions for the cluster.weave.works v1alpha3 API group 18 | // +kubebuilder:object:generate=true 19 | // +groupName=cluster.weave.works 20 | package v1alpha3 21 | 22 | import ( 23 | "k8s.io/apimachinery/pkg/runtime/schema" 24 | "sigs.k8s.io/controller-runtime/pkg/scheme" 25 | ) 26 | 27 | var ( 28 | // GroupVersion is group version used to register these objects 29 | GroupVersion = schema.GroupVersion{Group: "cluster.weave.works", Version: "v1alpha3"} 30 | 31 | // SchemeBuilder is used to add go types to the GroupVersionKind scheme 32 | SchemeBuilder = &scheme.Builder{GroupVersion: GroupVersion} 33 | 34 | // AddToScheme adds the types in this group-version to the given scheme. 35 | AddToScheme = SchemeBuilder.AddToScheme 36 | ) 37 | -------------------------------------------------------------------------------- /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/cluster.weave.works_existinginfraclusters.yaml 6 | - bases/cluster.weave.works_existinginframachines.yaml 7 | # - bases/baremetalproviderspec_clusterspecs.yaml 8 | # - bases/baremetalproviderspec_machinespecs.yaml 9 | # +kubebuilder:scaffold:crdkustomizeresource 10 | 11 | patchesStrategicMerge: 12 | # [WEBHOOK] To enable webhook, uncomment all the sections with [WEBHOOK] prefix. 13 | # patches here are for enabling the conversion webhook for each CRD 14 | #- patches/webhook_in_existinginfraclusters.yaml 15 | #- patches/webhook_in_existinginframachines.yaml 16 | #- patches/webhook_in_clusterspecs.yaml 17 | #- patches/webhook_in_machinespecs.yaml 18 | # +kubebuilder:scaffold:crdkustomizewebhookpatch 19 | 20 | # [CERTMANAGER] To enable webhook, uncomment all the sections with [CERTMANAGER] prefix. 21 | # patches here are for enabling the CA injection for each CRD 22 | #- patches/cainjection_in_existinginfraclusters.yaml 23 | #- patches/cainjection_in_existinginframachines.yaml 24 | #- patches/cainjection_in_clusterspecs.yaml 25 | #- patches/cainjection_in_machinespecs.yaml 26 | # +kubebuilder:scaffold:crdkustomizecainjectionpatch 27 | 28 | # the following config is for teaching kustomize how to do kustomization for CRDs. 29 | configurations: 30 | - kustomizeconfig.yaml 31 | -------------------------------------------------------------------------------- /pkg/plan/resource/run.go: -------------------------------------------------------------------------------- 1 | package resource 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | 7 | "github.com/pkg/errors" 8 | "github.com/weaveworks/cluster-api-provider-existinginfra/pkg/plan" 9 | ) 10 | 11 | // Run is a resource running a script (which can be just a single command). Run 12 | // doesn't realise any state, Apply will always run the given script. 13 | type Run struct { 14 | Base 15 | 16 | Script fmt.Stringer `structs:"script"` 17 | UndoScript fmt.Stringer `structs:"undoScript,omitempty"` 18 | UndoResource plan.Resource `structs:"undoResource,omitempty"` 19 | Output *string // for later resources to use 20 | } 21 | 22 | var _ plan.Resource = plan.RegisterResource(&Run{}) 23 | 24 | // State implements plan.Resource. 25 | func (r *Run) State() plan.State { 26 | return ToState(r) 27 | } 28 | 29 | // Apply implements plan.Resource. 30 | func (r *Run) Apply(ctx context.Context, runner plan.Runner, diff plan.Diff) (bool, error) { 31 | str, err := runner.RunCommand(ctx, r.Script.String(), nil) 32 | if r.Output != nil { 33 | *r.Output = str 34 | } 35 | if err != nil { 36 | return false, errors.Wrap(err, str) 37 | } 38 | return true, nil 39 | } 40 | 41 | // Undo implements plan.Resource. 42 | func (r *Run) Undo(ctx context.Context, runner plan.Runner, current plan.State) error { 43 | if r.UndoScript == nil { 44 | if r.UndoResource == nil { 45 | return nil 46 | } 47 | return r.UndoResource.Undo(ctx, runner, plan.EmptyState) 48 | } 49 | _, err := runner.RunCommand(ctx, r.UndoScript.String(), nil) 50 | return err 51 | } 52 | -------------------------------------------------------------------------------- /controllers/cluster.weave.works/helpers.go: -------------------------------------------------------------------------------- 1 | package controllers 2 | 3 | import ( 4 | "strings" 5 | 6 | log "github.com/sirupsen/logrus" 7 | metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" 8 | "k8s.io/apimachinery/pkg/runtime" 9 | clusterutil "sigs.k8s.io/cluster-api/util" 10 | "sigs.k8s.io/controller-runtime/pkg/event" 11 | "sigs.k8s.io/controller-runtime/pkg/predicate" 12 | ) 13 | 14 | func pausedPredicates() predicate.Funcs { 15 | return predicate.Funcs{ 16 | UpdateFunc: func(e event.UpdateEvent) bool { 17 | return processIfUnpaused(log.WithField("predicate", "updateEvent"), e.ObjectNew, e.MetaNew) 18 | }, 19 | CreateFunc: func(e event.CreateEvent) bool { 20 | return processIfUnpaused(log.WithField("predicate", "createEvent"), e.Object, e.Meta) 21 | }, 22 | DeleteFunc: func(e event.DeleteEvent) bool { 23 | return processIfUnpaused(log.WithField("predicate", "deleteEvent"), e.Object, e.Meta) 24 | }, 25 | GenericFunc: func(e event.GenericEvent) bool { 26 | return processIfUnpaused(log.WithField("predicate", "genericEvent"), e.Object, e.Meta) 27 | }, 28 | } 29 | } 30 | 31 | func processIfUnpaused(logger *log.Entry, obj runtime.Object, meta metav1.Object) bool { 32 | kind := strings.ToLower(obj.GetObjectKind().GroupVersionKind().Kind) 33 | log := logger.WithFields(log.Fields{"namespace": meta.GetNamespace(), kind: meta.GetName()}) 34 | if clusterutil.HasPausedAnnotation(meta) { 35 | log.Info("Resource is paused, will not attempt to map resource") 36 | return false 37 | } 38 | log.Info("Resource is not paused, will attempt to map resource") 39 | return true 40 | } 41 | -------------------------------------------------------------------------------- /apis/baremetalproviderspec/v1alpha1/machinespec_types.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2020 Weaveworks. 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 | // +kubebuilder:object:root=true 24 | 25 | // MachineSpec is the Schema for the machinespecs API 26 | type MachineSpec struct { 27 | metav1.TypeMeta `json:",inline"` 28 | // This ObjectMeta is not stored on encode, it is here just to provide 29 | // support for annotations that are required for comment preservation 30 | metav1.ObjectMeta `json:"-"` 31 | 32 | Address string `json:"address"` 33 | Port uint16 `json:"port,omitempty"` 34 | PrivateAddress string `json:"privateAddress,omitempty"` 35 | PrivateInterface string `json:"privateInterface,omitempty"` 36 | Private EndPoint `json:"private,omitempty"` 37 | Public EndPoint `json:"public,omitempty"` 38 | } 39 | 40 | // EndPoint groups the details required to establish a connection. 41 | type EndPoint struct { 42 | Address string `json:"address"` 43 | Port uint16 `json:"port"` 44 | } 45 | 46 | func init() { 47 | localSchemeBuilder.Register(addKnownMachineTypes) 48 | } 49 | -------------------------------------------------------------------------------- /pkg/plan/graph_test.go: -------------------------------------------------------------------------------- 1 | package plan 2 | 3 | import "testing" 4 | 5 | func index(s []string, v string) int { 6 | for i, s := range s { 7 | if s == v { 8 | return i 9 | } 10 | } 11 | return -1 12 | } 13 | 14 | type Edge struct { 15 | From string 16 | To string 17 | } 18 | 19 | func TestDuplicatedNode(t *testing.T) { 20 | graph := newGraph(2) 21 | graph.AddNode("a") 22 | if graph.AddNode("a") { 23 | t.Errorf("not raising duplicated node error") 24 | } 25 | 26 | } 27 | 28 | func TestRemoveNotExistEdge(t *testing.T) { 29 | graph := newGraph(0) 30 | if graph.RemoveEdge("a", "b") { 31 | t.Errorf("not raising not exist edge error") 32 | } 33 | } 34 | 35 | func TestWikipedia(t *testing.T) { 36 | graph := newGraph(8) 37 | graph.AddNodes("2", "3", "5", "7", "8", "9", "10", "11") 38 | 39 | edges := []Edge{ 40 | {"7", "8"}, 41 | {"7", "11"}, 42 | 43 | {"5", "11"}, 44 | 45 | {"3", "8"}, 46 | {"3", "10"}, 47 | 48 | {"11", "2"}, 49 | {"11", "9"}, 50 | {"11", "10"}, 51 | 52 | {"8", "9"}, 53 | } 54 | 55 | for _, e := range edges { 56 | graph.AddEdge(e.From, e.To) 57 | } 58 | 59 | result, ok := graph.Toposort() 60 | if !ok { 61 | t.Errorf("closed path detected in no closed pathed graph") 62 | } 63 | 64 | for _, e := range edges { 65 | if i, j := index(result, e.From), index(result, e.To); i > j { 66 | t.Errorf("dependency failed: not satisfy %v(%v) > %v(%v)", e.From, i, e.To, j) 67 | } 68 | } 69 | } 70 | 71 | func TestCycle(t *testing.T) { 72 | graph := newGraph(3) 73 | graph.AddNodes("1", "2", "3") 74 | 75 | graph.AddEdge("1", "2") 76 | graph.AddEdge("2", "3") 77 | graph.AddEdge("3", "1") 78 | 79 | _, ok := graph.Toposort() 80 | if ok { 81 | t.Errorf("closed path not detected in closed pathed graph") 82 | } 83 | } 84 | -------------------------------------------------------------------------------- /pkg/plan/resource/kubectl_annotate.go: -------------------------------------------------------------------------------- 1 | package resource 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | "strings" 7 | 8 | "github.com/pkg/errors" 9 | "github.com/weaveworks/cluster-api-provider-existinginfra/pkg/plan" 10 | ) 11 | 12 | // KubectlAnnotateSingleNode is a resource to apply an annotation to the only node in a cluster 13 | type KubectlAnnotateSingleNode struct { 14 | Base 15 | 16 | Key string // Which annotation to apply 17 | Value string // Value of annotation 18 | } 19 | 20 | var _ plan.Resource = plan.RegisterResource(&KubectlAnnotateSingleNode{}) 21 | 22 | // State implements plan.Resource. 23 | func (ka *KubectlAnnotateSingleNode) State() plan.State { 24 | return ToState(ka) 25 | } 26 | 27 | // Apply fetches the node name and performs a "kubectl annotate". 28 | func (ka *KubectlAnnotateSingleNode) Apply(ctx context.Context, runner plan.Runner, diff plan.Diff) (bool, error) { 29 | output, err := runner.RunCommand(ctx, WithoutProxy("kubectl get nodes -o name"), nil) 30 | if err != nil { 31 | return false, errors.Wrapf(err, "could not fetch node name to annotate") 32 | } 33 | 34 | nodeName := strings.Trim(output, " \n") 35 | if strings.Contains(nodeName, "\n") { 36 | return false, fmt.Errorf("unexpected output in node name: %q", output) 37 | } 38 | path, err := writeTempFile(ctx, runner, []byte(ka.Value), "node_annotation") 39 | if err != nil { 40 | return false, errors.Wrap(err, "writeTempFile") 41 | } 42 | //nolint:errcheck 43 | defer runner.RunCommand(ctx, fmt.Sprintf("rm -vf %q", path), nil) 44 | 45 | cmd := fmt.Sprintf("kubectl annotate %q %s=\"$(cat %s)\"", nodeName, ka.Key, path) 46 | 47 | if stdouterr, err := runner.RunCommand(ctx, WithoutProxy(cmd), nil); err != nil { 48 | return false, errors.Wrapf(err, "failed to apply annotation %s on %s; output %s", ka.Key, nodeName, stdouterr) 49 | } 50 | 51 | return true, nil 52 | } 53 | -------------------------------------------------------------------------------- /pkg/plan/builder_test.go: -------------------------------------------------------------------------------- 1 | package plan 2 | 3 | import ( 4 | "strings" 5 | "testing" 6 | 7 | "github.com/stretchr/testify/assert" 8 | ) 9 | 10 | func TestBuilder(t *testing.T) { 11 | b := NewBuilder() 12 | b.AddResource( 13 | "rpm:docker", 14 | &testResource{ID: "rpm:docker"}, 15 | ).AddResource( 16 | "service:docker", 17 | &testResource{ID: "service:docker"}, 18 | DependOn("rpm:docker", "file:daemon.json"), 19 | ).AddResource( 20 | "file:daemon.json", 21 | &testResource{ID: "file:daemon.json"}, 22 | DependOn("rpm:docker"), 23 | ) 24 | assert.Equal(t, 0, len(b.Errors())) 25 | 26 | plan, err := b.Plan() 27 | assert.NoError(t, err) 28 | sorted, ok := plan.graph.Toposort() 29 | assert.True(t, ok) 30 | assert.Equal(t, []string{"rpm:docker", "file:daemon.json", "service:docker"}, sorted) 31 | } 32 | 33 | func makeInvalidBuilder() *Builder { 34 | b := NewBuilder() 35 | b.AddResource( 36 | "rpm:docker", 37 | &testResource{ID: "rpm:docker"}, 38 | ).AddResource( 39 | "service:docker", 40 | &testResource{ID: "service:docker"}, 41 | DependOn("rpm:docker2", "file:daemon2.json"), 42 | ).AddResource( 43 | "file:daemon.json", 44 | &testResource{ID: "file:daemon.json"}, 45 | ) 46 | return b 47 | } 48 | 49 | func checkInvalidBuilderErrors(t *testing.T, b *Builder) { 50 | assert.Equal(t, 2, len(b.Errors())) 51 | errstr1 := b.Errors()[0].Error() 52 | errstr2 := b.Errors()[1].Error() 53 | assert.True(t, 54 | strings.Contains(errstr1, "rpm:docker2") || 55 | strings.Contains(errstr1, "file:daemon2.json")) 56 | assert.True(t, 57 | strings.Contains(errstr2, "rpm:docker2") || 58 | strings.Contains(errstr2, "file:daemon2.json")) 59 | } 60 | 61 | func TestInvalidBuilder(t *testing.T) { 62 | b := makeInvalidBuilder() 63 | _, _ = b.Plan() 64 | checkInvalidBuilderErrors(t, b) 65 | 66 | // run it again to make sure we don't create duplicate errors 67 | b = makeInvalidBuilder() 68 | _, _ = b.Plan() 69 | checkInvalidBuilderErrors(t, b) 70 | } 71 | -------------------------------------------------------------------------------- /pkg/apis/wksprovider/manifests/yaml/04_controller.yaml: -------------------------------------------------------------------------------- 1 | --- 2 | apiVersion: apps/v1 3 | kind: Deployment 4 | metadata: 5 | name: wks-controller 6 | namespace: system 7 | labels: 8 | name: wks-controller 9 | control-plane: wks-controller 10 | controller-tools.k8s.io: "1.0" 11 | spec: 12 | replicas: 1 13 | selector: 14 | matchLabels: 15 | name: wks-controller 16 | template: 17 | metadata: 18 | labels: 19 | name: wks-controller 20 | control-plane: wks-controller 21 | controller-tools.k8s.io: "1.0" 22 | spec: 23 | nodeSelector: 24 | node-role.kubernetes.io/master: "" 25 | tolerations: 26 | # Allow scheduling on master nodes. This is required because during 27 | # bootstrapping of the cluster, we may initially have just one master, 28 | # and would then need to deploy this controller there to set the entire 29 | # cluster up. 30 | - effect: NoSchedule 31 | key: node-role.kubernetes.io/master 32 | operator: Exists 33 | # Mark this as a critical addon: 34 | - key: CriticalAddonsOnly 35 | operator: Exists 36 | # Only schedule on nodes which are ready and reachable: 37 | - effect: NoExecute 38 | key: node.alpha.kubernetes.io/notReady 39 | operator: Exists 40 | - effect: NoExecute 41 | key: node.alpha.kubernetes.io/unreachable 42 | operator: Exists 43 | containers: 44 | - name: controller 45 | image: weaveworks/cluster-api-existinginfra-controller:v0.0.7 46 | args: 47 | - --verbose 48 | resources: 49 | limits: 50 | cpu: 100m 51 | memory: 100Mi 52 | requests: 53 | cpu: 100m 54 | memory: 20Mi 55 | volumeMounts: 56 | - name: ipPool 57 | mountPath: /var/ipPool 58 | readOnly: true 59 | volumes: 60 | - name: ipPool 61 | configMap: 62 | name: ipPool 63 | -------------------------------------------------------------------------------- /pkg/plan/resource/dir.go: -------------------------------------------------------------------------------- 1 | package resource 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | "strings" 7 | 8 | "github.com/weaveworks/cluster-api-provider-existinginfra/pkg/plan" 9 | ) 10 | 11 | // XXX: Expose file permission (if needed?) 12 | 13 | // Dir represents a directory on the file system. 14 | type Dir struct { 15 | // Path at which to create directory 16 | Path fmt.Stringer `structs:"path,omitempty"` 17 | // RecursiveDelete makes the undo operation recursive 18 | RecursiveDelete bool 19 | } 20 | 21 | var _ plan.Resource = plan.RegisterResource(&Dir{}) 22 | 23 | var protectedDirs = make(map[string]struct{}) 24 | 25 | func init() { 26 | for _, dir := range []string{"/", "/etc", "/var", "/dev", "/usr", "/root", "/home", "/opt", "/bin", "/sbin"} { 27 | protectedDirs[dir] = struct{}{} 28 | } 29 | } 30 | 31 | // State implements plan.Resource. 32 | func (d *Dir) State() plan.State { 33 | return ToState(d) 34 | } 35 | 36 | // QueryState implements plan.Resource. 37 | func (d *Dir) QueryState(ctx context.Context, runner plan.Runner) (plan.State, error) { 38 | return plan.EmptyState, nil 39 | } 40 | 41 | // Apply implements plan.Resource. 42 | func (d *Dir) Apply(ctx context.Context, runner plan.Runner, diff plan.Diff) (bool, error) { 43 | _, err := runner.RunCommand(ctx, fmt.Sprintf("mkdir -p %s", d.Path), nil) 44 | if err != nil { 45 | return false, err 46 | } 47 | return true, nil 48 | } 49 | 50 | // Undo implements plan.Resource. 51 | func (d *Dir) Undo(ctx context.Context, runner plan.Runner, current plan.State) error { 52 | path := strings.TrimRight(d.Path.String(), "/") 53 | if _, ok := protectedDirs[path]; ok { 54 | return fmt.Errorf("deletion aborted because dir is blacklisted for deletion: %s", path) 55 | } 56 | 57 | var cmd string 58 | if d.RecursiveDelete { 59 | cmd = fmt.Sprintf("rm -rvf -- %q", path) 60 | } else { 61 | cmd = fmt.Sprintf("[ ! -e %q ] || rmdir -v --ignore-fail-on-non-empty -- %q", path, path) 62 | } 63 | 64 | _, err := runner.RunCommand(ctx, cmd, nil) 65 | return err 66 | } 67 | -------------------------------------------------------------------------------- /pkg/plan/resource/dpkg.go: -------------------------------------------------------------------------------- 1 | package resource 2 | 3 | import ( 4 | "bufio" 5 | "context" 6 | "fmt" 7 | "strings" 8 | 9 | "github.com/weaveworks/cluster-api-provider-existinginfra/pkg/plan" 10 | ) 11 | 12 | // dpkgQuerier can ask dpkg about packages installed in the system. 13 | type dpkgQuerier struct { 14 | Runner plan.Runner 15 | // Command to use. Defaults to "dpkg-query". This is useful for testing. 16 | Command string 17 | } 18 | 19 | // debPkgInfo identifies a .deb package. 20 | type debPkgInfo struct { 21 | Name, Version string 22 | } 23 | 24 | func (d *dpkgQuerier) ShowInstalled(ctx context.Context, name string) ([]debPkgInfo, error) { 25 | // Run dpkg-query. 26 | const sep = "\t" 27 | formatFields := []string{"${Package}", "${Version}"} 28 | 29 | cmd := fmt.Sprintf("'%s' --showformat '%s' -W '%s'", 30 | d.CommandMaybeDefault(), strings.Join(formatFields, sep)+"\n", name) 31 | out, err := d.Runner.RunCommand(ctx, cmd, nil) 32 | 33 | // Handle "package not found". 34 | if err != nil { 35 | const ExitCodePkgNotFound = 1 36 | if err, ok := err.(*plan.RunError); ok && err.ExitCode == ExitCodePkgNotFound { 37 | return nil, nil 38 | } 39 | } 40 | 41 | // Handle runtime errors. 42 | if err != nil { 43 | return nil, fmt.Errorf("dpkg: command %q failed: %v", cmd, err) 44 | } 45 | 46 | // Parse the package list. 47 | var pkgs []debPkgInfo 48 | lines := bufio.NewScanner(strings.NewReader(out)) 49 | for lines.Scan() { 50 | line := lines.Text() 51 | fields := strings.SplitN(line, sep, len(formatFields)+1) 52 | if len(fields) != len(formatFields) { 53 | return nil, fmt.Errorf("cannot parse output line (bad number of fields): %v", line) 54 | } 55 | pkgs = append(pkgs, debPkgInfo{ 56 | Name: fields[0], 57 | Version: fields[1], 58 | }) 59 | } 60 | if err := lines.Err(); err != nil { 61 | return nil, err 62 | } 63 | 64 | return pkgs, nil 65 | } 66 | 67 | func (d *dpkgQuerier) CommandMaybeDefault() string { 68 | if d.Command == "" { 69 | return "dpkg-query" 70 | } 71 | return d.Command 72 | } 73 | -------------------------------------------------------------------------------- /pkg/scheme/scheme.go: -------------------------------------------------------------------------------- 1 | package scheme 2 | 3 | import ( 4 | ssv1alpha1 "github.com/bitnami-labs/sealed-secrets/pkg/apis/sealed-secrets/v1alpha1" 5 | capeiv1alpha1 "github.com/weaveworks/cluster-api-provider-existinginfra/apis/baremetalproviderspec/v1alpha1" 6 | capeiv1alpha3 "github.com/weaveworks/cluster-api-provider-existinginfra/apis/cluster.weave.works/v1alpha3" 7 | "github.com/weaveworks/libgitops/pkg/serializer" 8 | "k8s.io/apimachinery/pkg/runtime" 9 | "k8s.io/apimachinery/pkg/util/errors" 10 | utilruntime "k8s.io/apimachinery/pkg/util/runtime" 11 | clientgoscheme "k8s.io/client-go/kubernetes/scheme" 12 | clusterv1alpha3 "sigs.k8s.io/cluster-api/api/v1alpha3" 13 | ) 14 | 15 | var ( 16 | // Scheme contains information about all known types, API versions, and defaulting & conversion methods 17 | Scheme = runtime.NewScheme() 18 | 19 | // Serializer provides powerful high-level encoding/decoding functionality 20 | Serializer = serializer.NewSerializer(Scheme, nil) 21 | ) 22 | 23 | func init() { 24 | utilruntime.Must(AddToScheme(Scheme)) 25 | } 26 | 27 | // AddToScheme builds the scheme using all known versions of the API. 28 | func AddToScheme(scheme *runtime.Scheme) error { 29 | // This returns an error if and only if any of the following function calls return an error. 30 | // If many errors are returned, they are all concatenated after each other. 31 | return errors.NewAggregate([]error{ 32 | clientgoscheme.AddToScheme(scheme), // Register all known Kubernetes types 33 | ssv1alpha1.AddToScheme(scheme), // Register Bitnami's Sealed Secrets types 34 | capeiv1alpha1.AddToScheme(scheme), // Register baremetalproviderspec v1alpha1 types 35 | capeiv1alpha3.AddToScheme(scheme), // Register cluster.weave.works v1alpha3 types 36 | clusterv1alpha3.AddToScheme(scheme), // Register the upstream CAPI v1alpha3 types 37 | scheme.SetVersionPriority(capeiv1alpha3.GroupVersion), // Always prefer v1alpha3 when encoding our types 38 | }) 39 | } 40 | -------------------------------------------------------------------------------- /controllers/cluster.weave.works/existinginfrabootstrap_controller.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2020 Weaveworks. 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 | "context" 21 | 22 | "github.com/go-logr/logr" 23 | "k8s.io/apimachinery/pkg/runtime" 24 | ctrl "sigs.k8s.io/controller-runtime" 25 | "sigs.k8s.io/controller-runtime/pkg/client" 26 | 27 | clusterweaveworksv1alpha3 "github.com/weaveworks/cluster-api-provider-existinginfra/apis/cluster.weave.works/v1alpha3" 28 | ) 29 | 30 | // ExistingInfraBootstrapReconciler reconciles a ExistingInfraMachine object 31 | type ExistingInfraBootstrapReconciler struct { 32 | client.Client 33 | Log logr.Logger 34 | Scheme *runtime.Scheme 35 | } 36 | 37 | // +kubebuilder:rbac:groups=cluster.weave.works,resources=existinginfrabootstraps,verbs=get;list;watch;create;update;patch;delete 38 | // +kubebuilder:rbac:groups=cluster.weave.works,resources=existinginfrabootstraps/status,verbs=get;update;patch 39 | 40 | func (r *ExistingInfraBootstrapReconciler) Reconcile(req ctrl.Request) (ctrl.Result, error) { 41 | _ = context.Background() 42 | _ = r.Log.WithValues("existinginfrabootstrap", req.NamespacedName) 43 | 44 | // your logic here 45 | 46 | // The ExistingInfraMachine performs both the bootstrapping and infrastructure 47 | // support. 48 | 49 | return ctrl.Result{}, nil 50 | } 51 | 52 | func (r *ExistingInfraBootstrapReconciler) SetupWithManager(mgr ctrl.Manager) error { 53 | return ctrl.NewControllerManagedBy(mgr). 54 | For(&clusterweaveworksv1alpha3.ExistingInfraMachine{}). 55 | Complete(r) 56 | } 57 | -------------------------------------------------------------------------------- /pkg/utilities/version/version.go: -------------------------------------------------------------------------------- 1 | package version 2 | 3 | import ( 4 | "github.com/blang/semver" 5 | "github.com/pkg/errors" 6 | "k8s.io/apimachinery/pkg/util/version" 7 | ) 8 | 9 | const ( 10 | // AnyRange represents any versions range. 11 | AnyRange = "*" 12 | emptyRange = "" 13 | ) 14 | 15 | var ( 16 | // The constant below is to be set by flags passed to `go build`. 17 | // Example: -X version.Version=xxxxx 18 | 19 | Version = "undefined" 20 | ) 21 | 22 | // MatchesRange parses the provided version and versions range, and checks if 23 | // the provided version matches the provided range. 24 | func MatchesRange(version, versionsRange string) (bool, error) { 25 | if versionsRange == AnyRange || versionsRange == emptyRange { 26 | // Check specifically for the above special cases, as otherwise semver 27 | // fails with "Last element in range is '||'". 28 | return true, nil 29 | } 30 | v, err := semver.ParseTolerant(version) 31 | if err != nil { 32 | return false, errors.Wrapf(err, "invalid version \"%s\"", version) 33 | } 34 | r, err := semver.ParseRange(versionsRange) 35 | if err != nil { 36 | return false, errors.Wrapf(err, "invalid versions range \"%s\"", versionsRange) 37 | } 38 | return r(v), nil 39 | } 40 | 41 | func Jump(nodeVersion, machineVersion string) (bool, error) { 42 | nodemajor, nodeminor, err := parseVersion(nodeVersion) 43 | if err != nil { 44 | return false, err 45 | } 46 | machinemajor, machineminor, err := parseVersion(machineVersion) 47 | if err != nil { 48 | return false, err 49 | } 50 | return machinemajor == nodemajor && machineminor-nodeminor > 1, nil 51 | } 52 | 53 | func LessThan(s1, s2 string) (bool, error) { 54 | v1, err := version.ParseSemantic(s1) 55 | if err != nil { 56 | return false, err 57 | } 58 | v2, err := version.ParseSemantic(s2) 59 | if err != nil { 60 | return false, err 61 | } 62 | return v1.LessThan(v2), nil 63 | } 64 | 65 | func parseVersion(s string) (int, int, error) { 66 | v, err := version.ParseSemantic(s) 67 | if err != nil { 68 | return -1, -1, errors.Wrap(err, "invalid kubernetes version") 69 | } 70 | return int(v.Major()), int(v.Minor()), nil 71 | } 72 | -------------------------------------------------------------------------------- /pkg/plan/builder.go: -------------------------------------------------------------------------------- 1 | package plan 2 | 3 | import ( 4 | "bytes" 5 | "errors" 6 | "fmt" 7 | "strings" 8 | 9 | "k8s.io/apimachinery/pkg/util/uuid" 10 | ) 11 | 12 | // Builder is a plan builder. 13 | type Builder struct { 14 | plan *Plan 15 | errors []error 16 | } 17 | 18 | // NewBuilder creates a new Builder. 19 | func NewBuilder(idSegments ...string) *Builder { 20 | p := newPlan() 21 | if len(idSegments) == 0 { 22 | p.id = fmt.Sprintf("plan-%s", uuid.NewUUID()) 23 | } else { 24 | p.id = strings.Join(idSegments, "-") 25 | } 26 | return &Builder{plan: p} 27 | } 28 | 29 | type addOptions struct { 30 | deps []string 31 | } 32 | 33 | // DependOn expresses dependency between Resources. 34 | func DependOn(dep string, deps ...string) func(*addOptions) { 35 | return func(o *addOptions) { 36 | o.deps = append(o.deps, dep) 37 | o.deps = append(o.deps, deps...) 38 | } 39 | } 40 | 41 | // AddResource adds a resource to the plan. 42 | func (b *Builder) AddResource(id string, r Resource, options ...func(*addOptions)) *Builder { 43 | o := &addOptions{} 44 | for _, option := range options { 45 | option(o) 46 | } 47 | if err := b.plan.addResource(id, r, o); err != nil { 48 | b.errors = append(b.errors, err) 49 | } 50 | return b 51 | } 52 | 53 | func (b *Builder) checkIfValidResource(n string) { 54 | _, ok := b.plan.resources[n] 55 | if !ok { 56 | err := fmt.Errorf("graph node %s is not a valid resource", n) 57 | b.errors = append(b.errors, err) 58 | } 59 | } 60 | 61 | func (b *Builder) validateGraph() { 62 | for n := range b.plan.graph.nodes { 63 | b.checkIfValidResource(n) 64 | } 65 | } 66 | 67 | // Errors returns the errors that occurred during the build. The user is 68 | // expected to check the return value of this function before using the Plan. 69 | func (b *Builder) Errors() []error { 70 | return b.errors 71 | } 72 | 73 | // Plan returns the built Plan. 74 | func (b *Builder) Plan() (Plan, error) { 75 | b.validateGraph() 76 | if len(b.errors) > 0 { 77 | var errs bytes.Buffer 78 | fmt.Fprintf(&errs, "Invalid plan:\n") 79 | for _, err := range b.Errors() { 80 | fmt.Fprintf(&errs, " %s\n", err.Error()) 81 | } 82 | return *b.plan, errors.New(errs.String()) 83 | } 84 | return *b.plan, nil 85 | } 86 | -------------------------------------------------------------------------------- /apis/baremetalproviderspec/v1alpha1/groupversion_info.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2020 Weaveworks. 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 baremetalproviderspec v1alpha1 API group 18 | // +kubebuilder:object:generate=true 19 | // +groupName=baremetalproviderspec 20 | package v1alpha1 21 | 22 | import ( 23 | "k8s.io/apimachinery/pkg/runtime" 24 | "k8s.io/apimachinery/pkg/runtime/schema" 25 | "sigs.k8s.io/controller-runtime/pkg/scheme" 26 | ) 27 | 28 | var ( 29 | // GroupVersion is group version used to register these objects 30 | GroupVersion = schema.GroupVersion{Group: "baremetalproviderspec", Version: "v1alpha1"} 31 | 32 | // SchemeBuilder is used to add go types to the GroupVersionKind scheme 33 | SchemeBuilder = &scheme.Builder{GroupVersion: GroupVersion} 34 | 35 | // AddToScheme adds the types in this group-version to the given scheme. 36 | AddToScheme = SchemeBuilder.AddToScheme 37 | 38 | // localSchemeBuilder is a pointer to the runtime.SchemeBuilder used to add known types and conversions. 39 | localSchemeBuilder = &SchemeBuilder.SchemeBuilder 40 | ) 41 | 42 | func addKnownClusterTypes(scheme *runtime.Scheme) error { 43 | // BareMetalClusterProviderSpec is internally called 44 | // ClusterSpec to enable automatic conversions to v1alpha3. 45 | scheme.AddKnownTypeWithName(GroupVersion.WithKind("BareMetalClusterProviderSpec"), &ClusterSpec{}) 46 | 47 | return nil 48 | } 49 | 50 | func addKnownMachineTypes(scheme *runtime.Scheme) error { 51 | // BareMetalMachineProviderSpec is internally called 52 | // MachineSpec to enable automatic conversions to v1alpha3. 53 | scheme.AddKnownTypeWithName(GroupVersion.WithKind("BareMetalMachineProviderSpec"), &MachineSpec{}) 54 | 55 | return nil 56 | } 57 | -------------------------------------------------------------------------------- /pkg/plan/resource/deb.go: -------------------------------------------------------------------------------- 1 | package resource 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | 7 | "github.com/weaveworks/cluster-api-provider-existinginfra/pkg/plan" 8 | ) 9 | 10 | // Deb represents a .deb package. 11 | type Deb struct { 12 | Name string `structs:"name"` 13 | // Suffix is either "=" followed by the version, or "/" followed by the release stream (stable|testing|unstable). 14 | // Examples: 15 | // Name: "busybox" 16 | // Name: "busybox", Suffix: "/stable" 17 | // Name: "busybox", Suffix: "=1:1.27.2-2ubuntu3.2" 18 | Suffix string `structs:"suffix"` 19 | } 20 | 21 | var _ plan.Resource = plan.RegisterResource(&Deb{}) 22 | 23 | func (d *Deb) State() plan.State { 24 | return ToState(d) 25 | } 26 | 27 | func (d *Deb) QueryState(ctx context.Context, runner plan.Runner) (plan.State, error) { 28 | q := dpkgQuerier{Runner: runner} 29 | installed, err := q.ShowInstalled(ctx, d.Name) 30 | 31 | if err != nil { 32 | return nil, err 33 | } 34 | 35 | if len(installed) == 0 { 36 | return plan.EmptyState, nil 37 | } 38 | 39 | return DebResourceFromPackage(installed[0]).State(), nil 40 | } 41 | 42 | func (d *Deb) Apply(ctx context.Context, runner plan.Runner, diff plan.Diff) (propagate bool, err error) { 43 | a := aptInstaller{Runner: runner} 44 | if err := a.UpdateCache(ctx); err != nil { 45 | return false, fmt.Errorf("update cache failed: %v", err) 46 | } 47 | 48 | if err := a.Install(ctx, d.Name, d.Suffix); err != nil { 49 | return false, err 50 | } 51 | 52 | return true, nil 53 | } 54 | 55 | func (d *Deb) Undo(ctx context.Context, runner plan.Runner, current plan.State) error { 56 | a := aptInstaller{Runner: runner} 57 | if err := a.Purge(ctx, d.Name); err != nil { 58 | fmt.Printf("Failed to remove package: %s\n", d.Name) 59 | } 60 | return nil 61 | } 62 | 63 | func DebResourceFromPackage(p debPkgInfo) *Deb { 64 | return &Deb{ 65 | Name: p.Name, 66 | Suffix: "=" + p.Version, 67 | } 68 | } 69 | 70 | // WouldChangeState returns false if it's guaranteed that a call to Apply() wouldn't change the package installed, and true otherwise. 71 | func (d *Deb) WouldChangeState(ctx context.Context, r plan.Runner) (bool, error) { 72 | current, err := d.QueryState(ctx, r) 73 | if err != nil { 74 | return false, err 75 | } 76 | return !current.Equal(d.State()), nil 77 | } 78 | -------------------------------------------------------------------------------- /pkg/utilities/kubeadm/bootstrap_token.go: -------------------------------------------------------------------------------- 1 | package kubeadm 2 | 3 | import ( 4 | "fmt" 5 | "strings" 6 | "time" 7 | 8 | corev1 "k8s.io/api/core/v1" 9 | metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" 10 | bootstrapapi "k8s.io/cluster-bootstrap/token/api" 11 | kubeadmutil "k8s.io/cluster-bootstrap/token/util" 12 | kubeadmapi "k8s.io/kubernetes/cmd/kubeadm/app/apis/kubeadm/v1beta1" 13 | kubeadmconstants "k8s.io/kubernetes/cmd/kubeadm/app/constants" 14 | ) 15 | 16 | // GenerateBootstrapToken generates a new kubeadm bootstrap token, used by 17 | // kubeadm init and kubeadm join to safely form clusters. 18 | func GenerateBootstrapToken() (*kubeadmapi.BootstrapTokenString, error) { 19 | token, err := kubeadmutil.GenerateBootstrapToken() 20 | if err != nil { 21 | return nil, err 22 | } 23 | idAndSecret := strings.Split(token, ".") 24 | if len(idAndSecret) != 2 { 25 | // Never supposed to happen with the current bootstrap token format, 26 | // but we nevertheless defend ourselves against it. 27 | return nil, fmt.Errorf("invalid kubeadm bootstrap token: %s", token) 28 | } 29 | return &kubeadmapi.BootstrapTokenString{ 30 | ID: idAndSecret[0], 31 | Secret: idAndSecret[1], 32 | }, nil 33 | } 34 | 35 | // GenerateBootstrapSecret creates a new bootstrap secret to be used with kubeadm join. This secret must be 36 | // applied to the kubernetes cluster prior to trying to join a node to the cluster. 37 | func GenerateBootstrapSecret(namespace string) (*corev1.Secret, error) { 38 | t, err := GenerateBootstrapToken() 39 | if err != nil { 40 | return nil, err 41 | } 42 | d := map[string]string{ 43 | bootstrapapi.BootstrapTokenExtraGroupsKey: kubeadmconstants.NodeBootstrapTokenAuthGroup, 44 | bootstrapapi.BootstrapTokenIDKey: t.ID, 45 | bootstrapapi.BootstrapTokenSecretKey: t.Secret, 46 | bootstrapapi.BootstrapTokenUsageAuthentication: "true", 47 | bootstrapapi.BootstrapTokenUsageSigningKey: "true", 48 | bootstrapapi.BootstrapTokenExpirationKey: time.Now().Add(time.Hour * 24).Format(time.RFC3339), 49 | } 50 | n := fmt.Sprintf("%s%s", bootstrapapi.BootstrapTokenSecretPrefix, t.ID) 51 | return &corev1.Secret{ 52 | Type: corev1.SecretTypeBootstrapToken, 53 | StringData: d, 54 | ObjectMeta: metav1.ObjectMeta{ 55 | Name: n, 56 | Namespace: namespace, 57 | }, 58 | }, nil 59 | } 60 | -------------------------------------------------------------------------------- /apis/baremetalproviderspec/v1alpha1/conversion.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2020 Weaveworks. 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 | "errors" 21 | 22 | "github.com/weaveworks/cluster-api-provider-existinginfra/apis/cluster.weave.works/v1alpha3" 23 | "k8s.io/apimachinery/pkg/conversion" 24 | ) 25 | 26 | var errDowngradingConversion = errors.New("invalid conversion, downgrading conversions are not supported") 27 | 28 | func Convert_v1alpha1_APIServer_To_v1alpha3_APIServer(_ *APIServer, _ *v1alpha3.APIServer, _ conversion.Scope) error { 29 | // in.ExternalLoadBalancer is unused in v1alpha3 30 | return nil 31 | } 32 | 33 | func Convert_v1alpha1_ClusterSpec_To_v1alpha3_ClusterSpec(in *ClusterSpec, out *v1alpha3.ClusterSpec, s conversion.Scope) error { 34 | // in.ObjectMeta is only used for preserving comments 35 | return autoConvert_v1alpha1_ClusterSpec_To_v1alpha3_ClusterSpec(in, out, s) 36 | } 37 | 38 | func Convert_v1alpha3_ClusterSpec_To_v1alpha1_ClusterSpec(_ *v1alpha3.ClusterSpec, _ *ClusterSpec, _ conversion.Scope) error { 39 | // Downgrading conversions are not supported 40 | return errDowngradingConversion 41 | } 42 | 43 | func Convert_v1alpha1_MachineSpec_To_v1alpha3_MachineSpec(in *MachineSpec, out *v1alpha3.MachineSpec, s conversion.Scope) error { 44 | // in.ObjectMeta is only used for preserving comments 45 | // in.Address is unused in v1alpha3 46 | // in.Port is unused in v1alpha3 47 | // in.PrivateAddress is unused in v1alpha3 48 | // in.PrivateInterface is unused in v1alpha3 49 | return autoConvert_v1alpha1_MachineSpec_To_v1alpha3_MachineSpec(in, out, s) 50 | } 51 | 52 | func Convert_v1alpha3_MachineSpec_To_v1alpha1_MachineSpec(_ *v1alpha3.MachineSpec, _ *MachineSpec, _ conversion.Scope) error { 53 | // Downgrading conversions are not supported 54 | return errDowngradingConversion 55 | } 56 | -------------------------------------------------------------------------------- /pkg/plan/resource_test.go: -------------------------------------------------------------------------------- 1 | package plan 2 | 3 | import ( 4 | "context" 5 | "errors" 6 | "reflect" 7 | 8 | "k8s.io/apimachinery/pkg/util/uuid" 9 | ) 10 | 11 | // base can be embedded into a struct to provide a default implementation of 12 | // plan.Resource. 13 | type testResource struct { 14 | ID string 15 | DesiredState State 16 | StateValue State 17 | QueryShouldError bool 18 | QueryShouldErrorAfterApply bool 19 | ApplyShouldError bool 20 | UndoShouldError bool 21 | StatesShouldNotMatch bool 22 | ApplyShouldNotFix bool 23 | ApplyShouldNotPropagate bool 24 | } 25 | 26 | var _ Resource = RegisterResource(&testResource{}) 27 | var UnmatchableState = State(map[string]interface{}{"statedata": uuid.NewUUID()}) 28 | 29 | // State implements Resource. 30 | func (r *testResource) State() State { 31 | if r.StatesShouldNotMatch { 32 | return UnmatchableState 33 | } 34 | 35 | if r.DesiredState == nil { 36 | return EmptyState 37 | } 38 | 39 | return r.DesiredState 40 | } 41 | 42 | // QueryState implements Resource. 43 | func (r *testResource) QueryState(_ context.Context, runner Runner) (State, error) { 44 | if r.QueryShouldError { 45 | return EmptyState, errors.New("Could not query state") 46 | } 47 | 48 | return r.StateValue, nil 49 | } 50 | 51 | // Apply implements Resource. 52 | func (r *testResource) Apply(_ context.Context, runner Runner, diff Diff) (bool, error) { 53 | if r.ApplyShouldError { 54 | return false, errors.New("Apply failed") 55 | } 56 | 57 | invalid := !reflect.DeepEqual(diff.CurrentState, r.State()) 58 | 59 | if r.QueryShouldErrorAfterApply { 60 | r.QueryShouldError = true 61 | } 62 | if !r.ApplyShouldNotFix { 63 | r.StatesShouldNotMatch = false 64 | if diff.CurrentState != nil { 65 | r.DesiredState = diff.CurrentState 66 | } else { 67 | r.DesiredState = r.StateValue 68 | } 69 | } 70 | 71 | if invalid { 72 | if r.ApplyShouldNotFix { 73 | return false, errors.New("Apply failed") 74 | } 75 | return !r.ApplyShouldNotPropagate, nil 76 | } 77 | 78 | return !r.ApplyShouldNotPropagate, nil 79 | } 80 | 81 | // Undo implements Resource. 82 | func (r *testResource) Undo(_ context.Context, runner Runner, current State) error { 83 | if r.UndoShouldError { 84 | return errors.New("Undo failed") 85 | } 86 | 87 | return nil 88 | } 89 | -------------------------------------------------------------------------------- /pkg/plan/recipe/install_plans_test.go: -------------------------------------------------------------------------------- 1 | package recipe 2 | 3 | import ( 4 | "fmt" 5 | "testing" 6 | 7 | "github.com/stretchr/testify/assert" 8 | 9 | "github.com/weaveworks/cluster-api-provider-existinginfra/pkg/flavors/eksd" 10 | "github.com/weaveworks/cluster-api-provider-existinginfra/pkg/plan" 11 | "github.com/weaveworks/cluster-api-provider-existinginfra/pkg/plan/resource" 12 | ) 13 | 14 | func TestBinInstallerPkgResouce(t *testing.T) { 15 | rpm := resource.RPM{Name: "goo", Version: "v2.3", DisableExcludes: "kubernetes"} 16 | deb := resource.Deb{Name: "goo", Suffix: "=v2.3-00"} 17 | tests := []struct { 18 | pkg resource.PkgType 19 | bin string 20 | ver string 21 | exp plan.Resource 22 | }{ 23 | {resource.PkgTypeRPM, "goo", "v2.3", &rpm}, 24 | {resource.PkgTypeRHEL, "goo", "v2.3", &rpm}, 25 | {resource.PkgTypeDeb, "goo", "v2.3", &deb}, 26 | } 27 | for _, test := range tests { 28 | f, err := BinInstaller(test.pkg, nil) 29 | assert.NoError(t, err) 30 | assert.NotNil(t, f) 31 | assert.NoError(t, err) 32 | assert.Equal(t, test.exp, f(test.bin, test.ver)) 33 | 34 | } 35 | } 36 | func TestBinInstallerFlavor(t *testing.T) { 37 | cf, err := eksd.New("https://distro.eks.amazonaws.com/kubernetes-1-18/kubernetes-1-18-eks-1.yaml") 38 | assert.NoError(t, err) 39 | 40 | f, err := BinInstaller(resource.PkgTypeRHEL, cf) 41 | assert.NoError(t, err) 42 | assert.NotNil(t, f) 43 | res, ok := f("kubelet", "v2.3").(*resource.Run) 44 | assert.True(t, ok) 45 | assert.Contains(t, res.Script, "curl -o /usr/bin/kubelet https") 46 | assert.Contains(t, res.Script, "chmod 755 /usr/bin/kubelet") 47 | 48 | res, ok = f("kubectl", "v2.3").(*resource.Run) 49 | assert.True(t, ok) 50 | assert.Contains(t, res.Script, "curl -o /usr/bin/kubectl https") 51 | assert.Contains(t, res.Script, "chmod 755 /usr/bin/kubectl") 52 | 53 | } 54 | 55 | func TestSHACheck(t *testing.T) { 56 | cf, err := eksd.New("https://distro.eks.amazonaws.com/kubernetes-1-18/kubernetes-1-18-eks-1.yaml") 57 | assert.NoError(t, err) 58 | 59 | f, err := BinInstaller(resource.PkgTypeDeb, cf) 60 | assert.NoError(t, err) 61 | assert.NotNil(t, f) 62 | res, ok := f("kubelet", "v2.3").(*resource.Run) 63 | _, sha256, err := cf.KubeBinURL("kubelet") 64 | assert.NoError(t, err) 65 | assert.True(t, ok) 66 | assert.Contains(t, res.Script, fmt.Sprintf("openssl dgst -sha256 /usr/bin/kubelet | grep \"%s\" > /dev/null", sha256)) 67 | } 68 | -------------------------------------------------------------------------------- /pkg/flavors/eksd/eksd_test.go: -------------------------------------------------------------------------------- 1 | package eksd 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/stretchr/testify/assert" 7 | ) 8 | 9 | func TestImageAndImageTag(t *testing.T) { 10 | // TODO replace with static text so we aren't relying on this URL 11 | e, err := New("https://distro.eks.amazonaws.com/kubernetes-1-18/kubernetes-1-18-eks-1.yaml") 12 | assert.NoError(t, err) 13 | tests := []struct { 14 | name string 15 | expRepo string 16 | expTag string 17 | expErr bool 18 | }{ 19 | {"etcd", "public.ecr.aws/eks-distro/etcd-io", "v3.4.14-eks-1-18-1", false}, 20 | {"eTCd", "public.ecr.aws/eks-distro/etcd-io", "v3.4.14-eks-1-18-1", false}, 21 | {"DNS", "public.ecr.aws/eks-distro/coredns", "v1.7.0-eks-1-18-1", false}, 22 | {"goober", "", "", true}, 23 | } 24 | for _, test := range tests { 25 | repo, tag, err := e.ImageInfo(test.name) 26 | if test.expErr { 27 | assert.Error(t, err) 28 | } else { 29 | assert.Equal(t, test.expRepo, repo) 30 | assert.Equal(t, test.expTag, tag) 31 | assert.NoError(t, err) 32 | } 33 | } 34 | } 35 | 36 | func TestKubeBin(t *testing.T) { 37 | // TODO replace with static text so we aren't relying on this URL 38 | e, err := New("https://distro.eks.amazonaws.com/kubernetes-1-18/kubernetes-1-18-eks-1.yaml") 39 | assert.NoError(t, err) 40 | tests := []struct { 41 | name string 42 | expErr bool 43 | }{ 44 | {"kubelet", false}, 45 | {"kubectl", false}, 46 | {"goober", true}, 47 | } 48 | for _, test := range tests { 49 | url, sha, err := e.KubeBinURL(test.name) 50 | if test.expErr { 51 | assert.Error(t, err) 52 | } else { 53 | assert.NotEqual(t, "", url) 54 | assert.NotEqual(t, "", sha) 55 | assert.NoError(t, err) 56 | } 57 | } 58 | } 59 | 60 | func TestKubeadmOverride(t *testing.T) { 61 | // TODO replace with static text so we aren't relying on this URL 62 | e, err := New("https://distro.eks.amazonaws.com/kubernetes-1-18/kubernetes-1-18-eks-1.yaml") 63 | assert.NoError(t, err) 64 | tests := []struct { 65 | name string 66 | expManifestURL string 67 | expErr bool 68 | }{ 69 | {"kubeadm", "https://weaveworks-wkp.s3.amazonaws.com/eks-d/kubeadm", false}, 70 | } 71 | for _, test := range tests { 72 | url, _, err := e.KubeBinURL(test.name) 73 | if test.expErr { 74 | assert.Error(t, err) 75 | } else { 76 | assert.NotEqual(t, "", url) 77 | assert.NoError(t, err) 78 | } 79 | } 80 | 81 | } 82 | -------------------------------------------------------------------------------- /config/default/kustomization.yaml: -------------------------------------------------------------------------------- 1 | # Adds namespace to all resources. 2 | namespace: cluster-api-provider-existinginfra-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: cluster-api-provider-existinginfra- 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 | # [WEBHOOK] To enable webhook, uncomment all the sections with [WEBHOOK] prefix including the one in 34 | # crd/kustomization.yaml 35 | #- manager_webhook_patch.yaml 36 | 37 | # [CERTMANAGER] To enable cert-manager, uncomment all sections with 'CERTMANAGER'. 38 | # Uncomment 'CERTMANAGER' sections in crd/kustomization.yaml to enable the CA injection in the admission webhooks. 39 | # 'CERTMANAGER' needs to be enabled to use ca injection 40 | #- webhookcainjection_patch.yaml 41 | 42 | # the following config is for teaching kustomize how to do var substitution 43 | vars: 44 | # [CERTMANAGER] To enable cert-manager, uncomment all sections with 'CERTMANAGER' prefix. 45 | #- name: CERTIFICATE_NAMESPACE # namespace of the certificate CR 46 | # objref: 47 | # kind: Certificate 48 | # group: cert-manager.io 49 | # version: v1alpha2 50 | # name: serving-cert # this name should match the one in certificate.yaml 51 | # fieldref: 52 | # fieldpath: metadata.namespace 53 | #- name: CERTIFICATE_NAME 54 | # objref: 55 | # kind: Certificate 56 | # group: cert-manager.io 57 | # version: v1alpha2 58 | # name: serving-cert # this name should match the one in certificate.yaml 59 | #- name: SERVICE_NAMESPACE # namespace of the service 60 | # objref: 61 | # kind: Service 62 | # version: v1 63 | # name: webhook-service 64 | # fieldref: 65 | # fieldpath: metadata.namespace 66 | #- name: SERVICE_NAME 67 | # objref: 68 | # kind: Service 69 | # version: v1 70 | # name: webhook-service 71 | -------------------------------------------------------------------------------- /apis/cluster.weave.works/v1alpha3/existinginframachine_types.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2020 Weaveworks. 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 v1alpha3 18 | 19 | import ( 20 | metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" 21 | ) 22 | 23 | // This runtime.Object/DeepCopy tag is here for MachineSpec, in order to facilitate easier 24 | // automated conversions from the earlier v1alpha1. 25 | // +kubebuilder:object:root=true 26 | 27 | // MachineSpec defines the desired state of ExistingInfraMachine 28 | type MachineSpec struct { 29 | // This TypeMeta is not stored on encode, it is here just 30 | // to provide runtime.Object compliance for conversion. 31 | metav1.TypeMeta `json:"-"` 32 | 33 | Private EndPoint `json:"private,omitempty"` 34 | Public EndPoint `json:"public,omitempty"` 35 | ProviderID string `json:"providerID,omitempty"` 36 | } 37 | 38 | // MachineStatus defines the observed state of ExistingInfraMachine 39 | type MachineStatus struct { 40 | Ready bool `json:"ready"` 41 | } 42 | 43 | // +kubebuilder:object:root=true 44 | // +kubebuilder:storageversion 45 | // +kubebuilder:subresource:status 46 | 47 | // ExistingInfraMachine is the Schema for the existinginframachines API 48 | type ExistingInfraMachine struct { 49 | metav1.TypeMeta `json:",inline"` 50 | metav1.ObjectMeta `json:"metadata,omitempty"` 51 | 52 | Spec MachineSpec `json:"spec,omitempty"` 53 | Status MachineStatus `json:"status,omitempty"` 54 | } 55 | 56 | // +kubebuilder:object:root=true 57 | 58 | // ExistingInfraMachineList contains a list of ExistingInfraMachine 59 | type ExistingInfraMachineList struct { 60 | metav1.TypeMeta `json:",inline"` 61 | metav1.ListMeta `json:"metadata,omitempty"` 62 | Items []ExistingInfraMachine `json:"items"` 63 | } 64 | 65 | // EndPoint groups the details required to establish a connection. 66 | type EndPoint struct { 67 | Address string `json:"address"` 68 | Port uint16 `json:"port"` 69 | } 70 | 71 | const ( 72 | // MachineFinalizer allows ReconcileExistingInfraMachine to clean up before 73 | // removing it from the apiserver. 74 | ExistingInfraMachineFinalizer = "existinginframachine.cluster.weave.works" 75 | ) 76 | 77 | func init() { 78 | SchemeBuilder.Register(&ExistingInfraMachine{}, &ExistingInfraMachineList{}) 79 | } 80 | -------------------------------------------------------------------------------- /config/crd/cluster.weave.works_existinginframachiness.yaml: -------------------------------------------------------------------------------- 1 | 2 | --- 3 | apiVersion: apiextensions.k8s.io/v1beta1 4 | kind: CustomResourceDefinition 5 | metadata: 6 | annotations: 7 | controller-gen.kubebuilder.io/version: v0.3.0 8 | creationTimestamp: null 9 | name: existinginframachines.cluster.weave.works 10 | labels: 11 | cluster.x-k8s.io/v1alpha3: v1alpha3 12 | spec: 13 | group: cluster.weave.works 14 | names: 15 | kind: ExistingInfraMachine 16 | listKind: ExistingInfraMachineList 17 | plural: existinginframachines 18 | singular: existinginframachine 19 | scope: Namespaced 20 | validation: 21 | openAPIV3Schema: 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 | properties: 37 | private: 38 | description: EndPoint groups the details required to establish a connection. 39 | properties: 40 | address: 41 | type: string 42 | port: 43 | type: integer 44 | required: 45 | - address 46 | - port 47 | type: object 48 | providerID: 49 | type: string 50 | public: 51 | description: EndPoint groups the details required to establish a connection. 52 | properties: 53 | address: 54 | type: string 55 | port: 56 | type: integer 57 | required: 58 | - address 59 | - port 60 | type: object 61 | type: object 62 | status: 63 | properties: 64 | ready: 65 | type: boolean 66 | required: 67 | - ready 68 | type: object 69 | type: object 70 | version: v1alpha3 71 | versions: 72 | - name: v1alpha3 73 | served: true 74 | storage: true 75 | status: 76 | acceptedNames: 77 | kind: "" 78 | plural: "" 79 | conditions: [] 80 | storedVersions: [] 81 | -------------------------------------------------------------------------------- /controllers/cluster.weave.works/suite_test.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2020 Weaveworks. 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 | clusterweaveworksv1alpha3 "github.com/weaveworks/cluster-api-provider-existinginfra/apis/cluster.weave.works/v1alpha3" 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(done Done) { 53 | logf.SetLogger(zap.LoggerTo(GinkgoWriter, true)) 54 | 55 | By("bootstrapping test environment") 56 | testEnv = &envtest.Environment{ 57 | CRDDirectoryPaths: []string{filepath.Join("..", "config", "crd", "bases")}, 58 | } 59 | 60 | var err error 61 | cfg, err = testEnv.Start() 62 | Expect(err).ToNot(HaveOccurred()) 63 | Expect(cfg).ToNot(BeNil()) 64 | 65 | err = clusterweaveworksv1alpha3.AddToScheme(scheme.Scheme) 66 | Expect(err).NotTo(HaveOccurred()) 67 | 68 | err = clusterweaveworksv1alpha3.AddToScheme(scheme.Scheme) 69 | Expect(err).NotTo(HaveOccurred()) 70 | 71 | err = clusterweaveworksv1alpha3.AddToScheme(scheme.Scheme) 72 | Expect(err).NotTo(HaveOccurred()) 73 | 74 | // +kubebuilder:scaffold:scheme 75 | 76 | k8sClient, err = client.New(cfg, client.Options{Scheme: scheme.Scheme}) 77 | Expect(err).ToNot(HaveOccurred()) 78 | Expect(k8sClient).ToNot(BeNil()) 79 | 80 | close(done) 81 | }, 60) 82 | 83 | var _ = AfterSuite(func() { 84 | By("tearing down the test environment") 85 | err := testEnv.Stop() 86 | Expect(err).ToNot(HaveOccurred()) 87 | }) 88 | -------------------------------------------------------------------------------- /controllers/cluster.weave.works/existinginframachine_controller_test.go: -------------------------------------------------------------------------------- 1 | package controllers 2 | 3 | import ( 4 | "context" 5 | "testing" 6 | "time" 7 | 8 | "github.com/stretchr/testify/assert" 9 | "github.com/weaveworks/cluster-api-provider-existinginfra/pkg/apis/wksprovider/machine/os" 10 | corev1 "k8s.io/api/core/v1" 11 | bootstrapapi "k8s.io/cluster-bootstrap/token/api" 12 | ) 13 | 14 | func TestJoinTokenExpirationHandling(t *testing.T) { 15 | checks := []struct { 16 | nowOffset time.Duration 17 | exp bool 18 | msg string 19 | }{ 20 | {nowOffset: (time.Hour * 1), exp: false, msg: "Token should be good for another hour"}, 21 | {nowOffset: (time.Second * 1), exp: true, msg: "Token expires in 30 seconds"}, 22 | {nowOffset: (time.Second * 59), exp: true, msg: "Token expires in 59 seconds"}, 23 | {nowOffset: (time.Second * 61), exp: false, msg: "Token good for 61 seconds - expiration limit is 60"}, 24 | } 25 | 26 | s := corev1.Secret{} 27 | for _, check := range checks { 28 | now := time.Now().Add(check.nowOffset) 29 | d := map[string][]byte{} 30 | d[bootstrapapi.BootstrapTokenExpirationKey] = []byte(now.Format(time.RFC3339)) 31 | s.Data = d 32 | assert.Equal(t, check.exp, bootstrapTokenHasExpired(&s), check.msg) 33 | } 34 | } 35 | 36 | func TestMissingSSHkey(t *testing.T) { 37 | ip := "10.10.2.3" 38 | foundInfo := os.MachineInfo{PrivateIP: ip} 39 | info1 := os.MachineInfo{SSHUser: "1", PrivateIP: "1"} 40 | info2 := os.MachineInfo{SSHUser: "2", PrivateIP: "2"} 41 | info3 := os.MachineInfo{SSHUser: "3", PrivateIP: "3"} 42 | checks := []struct { 43 | mi []os.MachineInfo 44 | ip string 45 | i os.MachineInfo 46 | hasError bool 47 | msg string 48 | }{ 49 | {mi: []os.MachineInfo{}, ip: "10.10.2.3", i: os.MachineInfo{}, hasError: true, msg: "No info objects"}, 50 | {mi: []os.MachineInfo{foundInfo}, ip: ip, i: foundInfo, hasError: false, msg: "Matching info"}, 51 | {mi: []os.MachineInfo{info1, info2, info3}, ip: ip, i: os.MachineInfo{}, hasError: true, msg: "No matching info and no common one"}, 52 | {mi: []os.MachineInfo{info1, foundInfo, info2, info3}, ip: ip, i: foundInfo, hasError: false, msg: "Matching info in a list"}, 53 | {mi: []os.MachineInfo{info1, info1, info1, info1}, ip: ip, i: info1, hasError: false, msg: "No matching info, but common info exists"}, 54 | {mi: []os.MachineInfo{info1}, ip: ip, i: info1, hasError: false, msg: "No matching info, but common info exists"}, 55 | } 56 | 57 | r := ExistingInfraMachineReconciler{} 58 | for _, check := range checks { 59 | t.Run(check.msg, func(t *testing.T) { 60 | i, err := r.getMachineInfoOrUseDefault(context.TODO(), &check.mi, check.ip) 61 | if check.hasError { 62 | assert.Error(t, err, check.msg) 63 | } else { 64 | assert.NoError(t, err, check.msg) 65 | assert.Equal(t, check.i, i, check.msg) 66 | } 67 | }) 68 | } 69 | } 70 | -------------------------------------------------------------------------------- /pkg/utilities/envcfg/environment_config.go: -------------------------------------------------------------------------------- 1 | package envcfg 2 | 3 | import ( 4 | "context" 5 | 6 | "github.com/pkg/errors" 7 | log "github.com/sirupsen/logrus" 8 | "github.com/weaveworks/cluster-api-provider-existinginfra/pkg/plan" 9 | "github.com/weaveworks/cluster-api-provider-existinginfra/pkg/plan/resource" 10 | ) 11 | 12 | type EnvSpecificConfig struct { 13 | ConntrackMax int32 14 | UseIPTables bool 15 | IgnorePreflightErrors []string 16 | SELinuxInstalled bool 17 | SetSELinuxPermissive bool 18 | DisableSwap bool 19 | LockYUMPkgs bool 20 | Namespace string 21 | HostnameOverride string 22 | } 23 | 24 | const ( 25 | FC_bridge_nf_call_iptables = `FileContent--proc-sys-net-bridge-bridge-nf-call-iptables` 26 | Swap = `Swap` 27 | SystemVerification = `SystemVerification` 28 | ) 29 | 30 | func getHostnameOverride(ctx context.Context, cloudProvider string, runner plan.Runner) (string, error) { 31 | switch cloudProvider { 32 | case "aws": 33 | return runner.RunCommand(ctx, "curl -s http://169.254.169.254/latest/meta-data/local-hostname 2>/dev/null", nil) 34 | default: 35 | return "", nil 36 | } 37 | } 38 | 39 | func GetEnvSpecificConfig(ctx context.Context, pkgType resource.PkgType, namespace string, cloudProvider string, r plan.Runner) (*EnvSpecificConfig, error) { 40 | osres, err := resource.NewOS(ctx, r) 41 | if err != nil { 42 | return nil, errors.Wrap(err, "NewOS") 43 | } 44 | seLinuxStatus, seLinuxMode, err := osres.GetSELinuxStatus(ctx) 45 | if err != nil { 46 | return nil, errors.Wrap(err, "GetSELinuxStatus") 47 | } 48 | 49 | inContainerVM, err := osres.IsOSInContainerVM(ctx) 50 | if err != nil { 51 | return nil, errors.Wrap(err, "IsOSInContainerVM") 52 | } 53 | 54 | hostnameOverride, err := getHostnameOverride(ctx, cloudProvider, r) 55 | if err != nil { 56 | return nil, err 57 | } 58 | 59 | ignorePreflightErrors := []string{} 60 | if inContainerVM { 61 | ignorePreflightErrors = []string{ 62 | FC_bridge_nf_call_iptables, 63 | Swap, 64 | SystemVerification, 65 | } 66 | } 67 | 68 | log.Debug("Current SELinuxMode: ", seLinuxMode) 69 | config := &EnvSpecificConfig{ 70 | ConntrackMax: 0, 71 | UseIPTables: !inContainerVM, 72 | SELinuxInstalled: seLinuxStatus.IsInstalled(), 73 | SetSELinuxPermissive: !inContainerVM && seLinuxStatus.IsInstalled() && !seLinuxMode.IsDisabled(), // if selinux is installed and not disabled, set it to permissive 74 | LockYUMPkgs: pkgType == resource.PkgTypeRPM, 75 | DisableSwap: !inContainerVM, 76 | IgnorePreflightErrors: ignorePreflightErrors, 77 | Namespace: namespace, 78 | HostnameOverride: hostnameOverride, 79 | } 80 | log.WithField("config", config).Debug("the following env-specific configuration will be used") 81 | return config, nil 82 | } 83 | -------------------------------------------------------------------------------- /pkg/utilities/ssh/ssh.go: -------------------------------------------------------------------------------- 1 | package ssh 2 | 3 | import ( 4 | "bufio" 5 | "io" 6 | "io/ioutil" 7 | "os" 8 | "path/filepath" 9 | "strings" 10 | 11 | "github.com/pkg/errors" 12 | "github.com/weaveworks/cluster-api-provider-existinginfra/pkg/utilities/path" 13 | "golang.org/x/crypto/ssh" 14 | ) 15 | 16 | // ReadPrivateKey reads the provided private SSH key file and returns the 17 | // corresponding bytes. 18 | func ReadPrivateKey(privateKeyPath string) ([]byte, error) { 19 | privateKeyPath, err := path.Expand(privateKeyPath) 20 | if err != nil { 21 | return nil, errors.Wrapf(err, "failed to expand path to private key \"%s\"", privateKeyPath) 22 | } 23 | privateKey, err := ioutil.ReadFile(privateKeyPath) 24 | if err != nil { 25 | return nil, errors.Wrapf(err, "failed to read private key \"%s\"", privateKeyPath) 26 | } 27 | return privateKey, nil 28 | } 29 | 30 | func SignerFromPrivateKey(privateKeyPath string, privateKey []byte) (ssh.Signer, error) { 31 | if len(privateKey) == 0 { 32 | var err error 33 | privateKey, err = ReadPrivateKey(privateKeyPath) 34 | if err != nil { 35 | return nil, err 36 | } 37 | } 38 | signer, err := ssh.ParsePrivateKey(privateKey) 39 | if err != nil { 40 | return nil, errors.Wrapf(err, "failed to parse private key \"%s\"", privateKeyPath) 41 | } 42 | return signer, nil 43 | } 44 | 45 | func HostKeyCallback(hostPublicKey ssh.PublicKey) ssh.HostKeyCallback { 46 | if hostPublicKey == nil { 47 | return ssh.InsecureIgnoreHostKey() 48 | } 49 | return ssh.FixedHostKey(hostPublicKey) 50 | } 51 | 52 | func HostPublicKey(host string) (ssh.PublicKey, error) { 53 | path := filepath.Join(os.Getenv("HOME"), ".ssh", "known_hosts") 54 | file, err := os.Open(path) 55 | if os.IsNotExist(err) { 56 | // return a nil error, as this is logically equivalent to having a file 57 | // without any public key, or without this host's public key, i.e.: 58 | // we still want to connect to this server. 59 | return nil, nil 60 | } else if err != nil { 61 | return nil, err 62 | } 63 | defer file.Close() 64 | return scanHostPublicKey(file, host) 65 | } 66 | 67 | func scanHostPublicKey(reader io.Reader, host string) (ssh.PublicKey, error) { 68 | scanner := bufio.NewScanner(reader) 69 | var hostKey ssh.PublicKey 70 | for scanner.Scan() { 71 | fields := strings.Split(sanitizeLine(scanner.Text()), " ") 72 | if len(fields) != 3 { 73 | continue 74 | } 75 | if strings.Contains(fields[0], host) { 76 | var err error 77 | hostKey, _, _, _, err = ssh.ParseAuthorizedKey(scanner.Bytes()) 78 | if err != nil { 79 | return nil, errors.Wrapf(err, "error parsing %q", fields[2]) 80 | } 81 | break 82 | } 83 | } 84 | return hostKey, nil 85 | } 86 | 87 | func sanitizeLine(line string) string { 88 | line = strings.Trim(line, " \t") 89 | if strings.HasPrefix(line, "#") { 90 | // Skip comments. 91 | return "" 92 | } 93 | return line 94 | } 95 | -------------------------------------------------------------------------------- /config/crd/bases/cluster.weave.works_existinginframachines.yaml: -------------------------------------------------------------------------------- 1 | 2 | --- 3 | apiVersion: apiextensions.k8s.io/v1beta1 4 | kind: CustomResourceDefinition 5 | metadata: 6 | annotations: 7 | controller-gen.kubebuilder.io/version: v0.3.0 8 | creationTimestamp: null 9 | name: existinginframachines.cluster.weave.works 10 | spec: 11 | group: cluster.weave.works 12 | names: 13 | kind: ExistingInfraMachine 14 | listKind: ExistingInfraMachineList 15 | plural: existinginframachines 16 | singular: existinginframachine 17 | scope: Namespaced 18 | subresources: 19 | status: {} 20 | validation: 21 | openAPIV3Schema: 22 | description: ExistingInfraMachine is the Schema for the existinginframachines 23 | API 24 | properties: 25 | apiVersion: 26 | description: 'APIVersion defines the versioned schema of this representation 27 | of an object. Servers should convert recognized schemas to the latest 28 | internal value, and may reject unrecognized values. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#resources' 29 | type: string 30 | kind: 31 | description: 'Kind is a string value representing the REST resource this 32 | object represents. Servers may infer this from the endpoint the client 33 | 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: MachineSpec defines the desired state of ExistingInfraMachine 39 | properties: 40 | private: 41 | description: EndPoint groups the details required to establish a connection. 42 | properties: 43 | address: 44 | type: string 45 | port: 46 | type: integer 47 | required: 48 | - address 49 | - port 50 | type: object 51 | providerID: 52 | type: string 53 | public: 54 | description: EndPoint groups the details required to establish a connection. 55 | properties: 56 | address: 57 | type: string 58 | port: 59 | type: integer 60 | required: 61 | - address 62 | - port 63 | type: object 64 | type: object 65 | status: 66 | description: MachineStatus defines the observed state of ExistingInfraMachine 67 | properties: 68 | ready: 69 | type: boolean 70 | required: 71 | - ready 72 | type: object 73 | type: object 74 | version: v1alpha3 75 | versions: 76 | - name: v1alpha3 77 | served: true 78 | storage: true 79 | status: 80 | acceptedNames: 81 | kind: "" 82 | plural: "" 83 | conditions: [] 84 | storedVersions: [] 85 | -------------------------------------------------------------------------------- /pkg/utilities/kubeadm/joincmd.go: -------------------------------------------------------------------------------- 1 | package kubeadm 2 | 3 | import ( 4 | "strings" 5 | 6 | "github.com/pkg/errors" 7 | flag "github.com/spf13/pflag" 8 | ) 9 | 10 | const ( 11 | kubeadmJoin = "kubeadm join" 12 | empty = "" 13 | newline = "\n" 14 | whitespace = " " 15 | backslash = "\\" 16 | // tokenDiscoveryCAHash flag instruct kubeadm to validate that the root CA public key matches this hash (for token-based discovery) 17 | tokenDiscoveryCAHash = "discovery-token-ca-cert-hash" 18 | // certificateKey flag sets the key used to encrypt and decrypt certificate secrets 19 | certificateKey = "certificate-key" 20 | ) 21 | 22 | // ExtractJoinCmd goes through the provided kubeadm init standard output and 23 | // extracts the kubeadm join command printed. 24 | func ExtractJoinCmd(stdOut string) (string, error) { 25 | // Another way to get the kubeadm join command is by creating another token 26 | // using: kubeadm token create --print-join-command 27 | // but given we cannot conveniently remove the previous token, via the CLI, 28 | // for now, we instead parse the output of kubeadm init to extract the join 29 | // command. 30 | lines := strings.Split(stdOut, newline) 31 | withinCmd := false 32 | var cmd strings.Builder 33 | for _, line := range lines { 34 | if strings.Contains(line, kubeadmJoin) { // Beginning of the command. 35 | cmd.WriteString(sanitize(line)) 36 | if hasLineContinuation(line) { 37 | cmd.WriteString(whitespace) 38 | withinCmd = true 39 | } else { 40 | break 41 | } 42 | } else if withinCmd { 43 | cmd.WriteString(sanitize(line)) 44 | if hasLineContinuation(line) { 45 | cmd.WriteString(whitespace) 46 | } else { 47 | break 48 | } 49 | } 50 | } 51 | if cmd.Len() > 0 { 52 | return cmd.String(), nil 53 | } 54 | return "", errors.New("kubeadm join command not found") 55 | } 56 | 57 | func sanitize(line string) string { 58 | line = strings.TrimRight(line, backslash) // Remove line continuation. 59 | line = strings.TrimSpace(line) 60 | return line 61 | } 62 | 63 | func hasLineContinuation(line string) bool { 64 | return strings.HasSuffix(line, backslash) 65 | } 66 | 67 | // ExtractDiscoveryTokenCaCertHash extracts the discover token CA cert hash 68 | // from the provided kubeadm join command. 69 | func ExtractDiscoveryTokenCaCertHash(kubeadmJoinCmd string) (string, error) { 70 | return extractFlag(kubeadmJoinCmd, tokenDiscoveryCAHash, "discovery token CA cert hash not found") 71 | } 72 | 73 | // ExtractCertificateKey extracts the certificate key from the provided kubeadm 74 | // join command. 75 | func ExtractCertificateKey(kubeadmJoinCmd string) (string, error) { 76 | return extractFlag(kubeadmJoinCmd, certificateKey, "certificate key not found") 77 | } 78 | 79 | func extractFlag(kubeadmJoinCmd, name, errorMessage string) (string, error) { 80 | cmd := strings.Split(kubeadmJoinCmd, whitespace) 81 | flagSet := flag.NewFlagSet(cmd[0], flag.ContinueOnError) 82 | value := flagSet.String(name, empty, empty) 83 | flagSet.ParseErrorsWhitelist.UnknownFlags = true // Ignore other flags. 84 | if err := flagSet.Parse(cmd[1:]); err != nil { 85 | return "", errors.Wrap(err, errorMessage) 86 | } 87 | return *value, nil 88 | } 89 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/weaveworks/cluster-api-provider-existinginfra 2 | 3 | go 1.13 4 | 5 | require ( 6 | github.com/aws/eks-distro-build-tooling/release v0.0.0-20201211225747-07f05a470de8 7 | github.com/bitnami-labs/sealed-secrets v0.12.5 8 | github.com/blang/semver v3.5.1+incompatible 9 | github.com/cavaliercoder/go-rpm v0.0.0-20200122174316-8cb9fd9c31a8 10 | github.com/chanwit/plandiff v1.0.0 11 | github.com/drone/envsubst v1.0.2 12 | github.com/fatih/structs v1.1.0 13 | github.com/go-logr/logr v0.3.0 14 | github.com/go-logr/zapr v0.2.0 // indirect 15 | github.com/oleiade/reflections v1.0.0 // indirect 16 | github.com/onsi/ginkgo v1.12.1 17 | github.com/onsi/gomega v1.10.1 18 | github.com/pkg/errors v0.9.1 19 | github.com/shurcooL/httpfs v0.0.0-20190707220628-8d4bc4ba7749 // indirect 20 | github.com/shurcooL/vfsgen v0.0.0-20200824052919-0d455de96546 // indirect 21 | github.com/sirupsen/logrus v1.6.0 22 | github.com/spf13/pflag v1.0.5 23 | github.com/stretchr/testify v1.6.1 24 | github.com/tj/assert v0.0.3 25 | github.com/weaveworks/footloose v0.0.0-20210208164054-2862489574a3 // indirect 26 | github.com/weaveworks/libgitops v0.0.2 27 | golang.org/x/crypto v0.0.0-20201016220609-9e8e0b390897 28 | gopkg.in/oleiade/reflections.v1 v1.0.0 29 | gopkg.in/yaml.v3 v3.0.0-20200615113413-eeeca48fe776 // indirect 30 | k8s.io/api v0.20.2 31 | k8s.io/apimachinery v0.20.2 32 | k8s.io/client-go v0.20.2 33 | k8s.io/cluster-bootstrap v0.20.2 34 | k8s.io/kube-proxy v0.0.0 35 | k8s.io/kubectl v0.20.2 36 | k8s.io/kubernetes v1.20.2 37 | k8s.io/utils v0.0.0-20201110183641-67b214c5f920 38 | sigs.k8s.io/cluster-api v0.3.6 39 | sigs.k8s.io/controller-runtime v0.6.0 40 | sigs.k8s.io/kind v0.9.0 // indirect 41 | sigs.k8s.io/kustomize/kyaml v0.4.2 42 | sigs.k8s.io/yaml v1.2.0 43 | ) 44 | 45 | replace ( 46 | k8s.io/api => k8s.io/api v0.20.2 47 | k8s.io/apiextensions-apiserver => k8s.io/apiextensions-apiserver v0.20.2 48 | k8s.io/apimachinery => k8s.io/apimachinery v0.20.2 49 | k8s.io/apiserver => k8s.io/apiserver v0.20.2 50 | k8s.io/cli-runtime => k8s.io/cli-runtime v0.20.2 51 | k8s.io/client-go => k8s.io/client-go v0.20.2 52 | k8s.io/cloud-provider => k8s.io/cloud-provider v0.20.2 53 | k8s.io/cluster-bootstrap => k8s.io/cluster-bootstrap v0.20.2 54 | k8s.io/code-generator => k8s.io/code-generator v0.20.2 55 | k8s.io/component-base => k8s.io/component-base v0.20.2 56 | k8s.io/component-helpers => k8s.io/component-helpers v0.20.2 57 | k8s.io/controller-manager => k8s.io/controller-manager v0.20.2 58 | k8s.io/cri-api => k8s.io/cri-api v0.20.2 59 | k8s.io/csi-translation-lib => k8s.io/csi-translation-lib v0.20.2 60 | k8s.io/kube-aggregator => k8s.io/kube-aggregator v0.20.2 61 | k8s.io/kube-controller-manager => k8s.io/kube-controller-manager v0.20.2 62 | k8s.io/kube-proxy => k8s.io/kube-proxy v0.20.2 63 | k8s.io/kube-scheduler => k8s.io/kube-scheduler v0.20.2 64 | k8s.io/kubectl => k8s.io/kubectl v0.20.2 65 | k8s.io/kubelet => k8s.io/kubelet v0.20.2 66 | k8s.io/legacy-cloud-providers => k8s.io/legacy-cloud-providers v0.20.2 67 | k8s.io/metrics => k8s.io/metrics v0.20.2 68 | k8s.io/mount-utils => k8s.io/mount-utils v0.20.3-rc.0 69 | k8s.io/sample-apiserver => k8s.io/sample-apiserver v0.20.2 70 | k8s.io/sample-cli-plugin => k8s.io/sample-cli-plugin v0.20.2 71 | k8s.io/sample-controller => k8s.io/sample-controller v0.20.2 72 | ) 73 | -------------------------------------------------------------------------------- /pkg/plan/graph.go: -------------------------------------------------------------------------------- 1 | package plan 2 | 3 | // This topological sort implementation from: 4 | // https://github.com/philopon/go-toposort 5 | // Copyright 2017 Hirotomo Moriwaki 6 | // MIT licensed. 7 | 8 | type graph struct { 9 | nodes map[string]bool 10 | edges map[string]map[string]bool 11 | outputs map[string]map[string]int 12 | inputs map[string]int 13 | } 14 | 15 | func newGraph(cap int) *graph { 16 | return &graph{ 17 | nodes: make(map[string]bool), 18 | edges: make(map[string]map[string]bool), 19 | inputs: make(map[string]int), 20 | outputs: make(map[string]map[string]int), 21 | } 22 | } 23 | 24 | func (g *graph) AddNode(name string) bool { 25 | if g.nodes[name] { 26 | return false 27 | } 28 | g.nodes[name] = true 29 | g.outputs[name] = make(map[string]int) 30 | g.inputs[name] = 0 31 | return true 32 | } 33 | 34 | func (g *graph) AddNodes(names ...string) bool { 35 | updated := false 36 | for _, name := range names { 37 | if ok := g.AddNode(name); ok { 38 | updated = true 39 | } 40 | } 41 | return updated 42 | } 43 | 44 | func (g *graph) AddEdge(from, to string) bool { 45 | edgesFrom := g.edges[from] 46 | if edgesFrom == nil { 47 | edgesFrom = make(map[string]bool) 48 | g.edges[from] = edgesFrom 49 | } else if edgesFrom[to] { 50 | return false 51 | } 52 | edgesFrom[to] = true 53 | g.AddNodes(from, to) 54 | m := g.outputs[from] 55 | m[to] = len(m) + 1 56 | g.inputs[to]++ 57 | return true 58 | } 59 | 60 | func (g *graph) nodeCopy() *graph { 61 | newg := newGraph(len(g.nodes)) 62 | for n := range g.nodes { 63 | newg.AddNode(n) 64 | } 65 | return newg 66 | } 67 | 68 | func (g *graph) Copy() *graph { 69 | newg := g.nodeCopy() 70 | for from, tos := range g.edges { 71 | for to := range tos { 72 | newg.AddEdge(from, to) 73 | } 74 | } 75 | return newg 76 | } 77 | 78 | func (g *graph) Invert() *graph { 79 | newg := g.nodeCopy() 80 | for from, tos := range g.edges { 81 | for to := range tos { 82 | newg.AddEdge(to, from) 83 | } 84 | } 85 | return newg 86 | } 87 | 88 | func (g *graph) unsafeRemoveEdge(from, to string) { 89 | edgesFrom := g.edges[from] 90 | if edgesFrom == nil { 91 | return 92 | } 93 | delete(edgesFrom, to) 94 | delete(g.outputs[from], to) 95 | g.inputs[to]-- 96 | } 97 | 98 | func (g *graph) RemoveEdge(from, to string) bool { 99 | if _, ok := g.outputs[from]; !ok { 100 | return false 101 | } 102 | g.unsafeRemoveEdge(from, to) 103 | return true 104 | } 105 | 106 | func (g *graph) Toposort() ([]string, bool) { 107 | newg := g.Copy() 108 | L := make([]string, 0, len(newg.nodes)) 109 | S := make([]string, 0, len(newg.nodes)) 110 | 111 | for n := range newg.nodes { 112 | if newg.inputs[n] == 0 { 113 | S = append(S, n) 114 | } 115 | } 116 | 117 | for len(S) > 0 { 118 | var n string 119 | n, S = S[0], S[1:] 120 | L = append(L, n) 121 | 122 | ms := make([]string, len(newg.outputs[n])) 123 | for m, i := range newg.outputs[n] { 124 | ms[i-1] = m 125 | } 126 | 127 | for _, m := range ms { 128 | newg.unsafeRemoveEdge(n, m) 129 | 130 | if newg.inputs[m] == 0 { 131 | S = append(S, m) 132 | } 133 | } 134 | } 135 | 136 | N := 0 137 | for _, v := range newg.inputs { 138 | N += v 139 | } 140 | 141 | if N > 0 { 142 | return L, false 143 | } 144 | 145 | return L, true 146 | } 147 | -------------------------------------------------------------------------------- /pkg/plan/resource/apt.go: -------------------------------------------------------------------------------- 1 | package resource 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | "io" 7 | "math/rand" 8 | "strings" 9 | "time" 10 | 11 | "github.com/weaveworks/cluster-api-provider-existinginfra/pkg/plan" 12 | 13 | log "github.com/sirupsen/logrus" 14 | ) 15 | 16 | type aptInstaller struct { 17 | Runner plan.Runner 18 | // Command to use. Defaults to "apt-get". This is useful for testing. 19 | Command string 20 | } 21 | 22 | const env = "LC_ALL=C DEBIAN_FRONTEND=noninteractive" 23 | 24 | func (a *aptInstaller) UpdateCache(ctx context.Context) error { 25 | flags := "--yes --quiet" 26 | cmd := fmt.Sprintf("%s '%s' %s update", env, a.CommandMaybeDefault(), flags) 27 | if out, err := wrapRetry(a.Runner).RunCommand(ctx, cmd, nil); err != nil { 28 | return fmt.Errorf("command %q failed: %v; output: %s", cmd, err, out) 29 | } 30 | return nil 31 | } 32 | 33 | func (a *aptInstaller) Install(ctx context.Context, name, suffix string) error { 34 | flags := "--yes --quiet --verbose-versions --no-install-recommends --allow-downgrades -o DPkg::Options::='--force-confnew'" 35 | cmd := fmt.Sprintf("%s '%s' %s install '%s%s'", env, a.CommandMaybeDefault(), flags, name, suffix) 36 | if out, err := wrapRetry(a.Runner).RunCommand(ctx, cmd, nil); err != nil { 37 | return fmt.Errorf("command %q failed: %v; output: %s", cmd, err, out) 38 | } 39 | return nil 40 | } 41 | 42 | func (a *aptInstaller) Purge(ctx context.Context, name string) error { 43 | flags := "--yes --quiet --verbose-versions --auto-remove" 44 | cmd := fmt.Sprintf("%s '%s' %s purge '%s'", env, a.CommandMaybeDefault(), flags, name) 45 | out, err := wrapRetry(a.Runner).RunCommand(ctx, cmd, nil) 46 | 47 | if _, ok := err.(*plan.RunError); ok { 48 | if strings.Contains(out, "E: Unable to locate package "+name) { 49 | return nil // the package isn't known to the package manager, so it's not installed - success 50 | } 51 | 52 | return fmt.Errorf("command %q failed: %v; output: %s", cmd, err, out) 53 | } 54 | 55 | return nil 56 | } 57 | 58 | func (a *aptInstaller) CommandMaybeDefault() string { 59 | if a.Command == "" { 60 | return "apt-get" 61 | } 62 | return a.Command 63 | } 64 | 65 | type retryingRunner struct { 66 | Runner plan.Runner 67 | Retries int 68 | } 69 | 70 | func wrapRetry(r plan.Runner) plan.Runner { 71 | return &retryingRunner{ 72 | Runner: r, 73 | Retries: 30, 74 | } 75 | } 76 | 77 | func (r *retryingRunner) RunCommand(ctx context.Context, cmd string, stdin io.Reader) (string, error) { 78 | return r.runOp(func() (string, error) { return r.Runner.RunCommand(ctx, cmd, stdin) }) 79 | } 80 | 81 | func (r *retryingRunner) runOp(op func() (string, error)) (string, error) { 82 | var out string 83 | var err error 84 | 85 | for i := 0; i < r.Retries; i++ { 86 | out, err = op() 87 | if _, ok := err.(*plan.RunError); !ok { 88 | // Not the case that our command returned a non-zero status. Hence not retrying. 89 | return out, err 90 | } 91 | 92 | if !strings.Contains(out, "Resource temporarily unavailable") { 93 | // Not the kind of error we want to retry on. Hence not retrying. 94 | return out, err 95 | } 96 | 97 | sleep := time.Duration(rand.Intn(10000)) * time.Millisecond 98 | log.Debugf("Retry #%d: Sleeping for %v", i, sleep) 99 | time.Sleep(sleep) 100 | } 101 | 102 | return out, err 103 | } 104 | -------------------------------------------------------------------------------- /pkg/flavors/eksd/eksd.go: -------------------------------------------------------------------------------- 1 | package eksd 2 | 3 | import ( 4 | "fmt" 5 | "io/ioutil" 6 | "net/http" 7 | "strings" 8 | 9 | distrov1alpha1 "github.com/aws/eks-distro-build-tooling/release/api/v1alpha1" 10 | gerrors "github.com/pkg/errors" 11 | "sigs.k8s.io/yaml" 12 | ) 13 | 14 | // EKSD is used to wrap the eks-d manifest and provide helper functions 15 | type EKSD struct { 16 | releaseURL string 17 | release *distrov1alpha1.Release 18 | } 19 | 20 | const ( 21 | // Flavor name of the eks-d flavor 22 | Flavor string = "eks-d" 23 | ) 24 | 25 | // New create an instance of EKSD with the manifest from the URL argument 26 | func New(eksdURL string) (*EKSD, error) { 27 | m, err := readRelease(eksdURL) 28 | if err != nil { 29 | return nil, gerrors.Wrap(err, fmt.Sprintf("failed to create EKSD using manifest %s", eksdURL)) 30 | } 31 | return &EKSD{ 32 | releaseURL: eksdURL, 33 | release: m, 34 | }, nil 35 | } 36 | 37 | // KubeBinURL looks through the eksd manifest and locates the URL for the 38 | // binary 39 | func (e *EKSD) KubeBinURL(binName string) (url string, sha256 string, err error) { 40 | c := e.findComponent("kubernetes") 41 | if c == nil { 42 | return "", "", fmt.Errorf("component %s not found in release %v", binName, e.release.Spec) 43 | } 44 | for _, a := range c.Assets { 45 | if a.Name == "bin/linux/amd64/"+binName { 46 | return a.Archive.URI, a.Archive.SHA256, nil 47 | } 48 | } 49 | if binName == "kubeadm" { 50 | return overrideKubeadm() 51 | } 52 | return "", "", fmt.Errorf("binary %s not found in release %v", binName, e.release.Spec) 53 | } 54 | 55 | func (e *EKSD) findComponent(name string) *distrov1alpha1.Component { 56 | n := strings.ToLower(name) 57 | for _, c := range e.release.Status.Components { 58 | if strings.ToLower(c.Name) == n { 59 | return &c 60 | } 61 | } 62 | return nil 63 | 64 | } 65 | func overrideKubeadm() (string, string, error) { 66 | return "https://weaveworks-wkp.s3.amazonaws.com/eks-d/kubeadm", "", nil 67 | } 68 | 69 | // ImageInfo returns the image repo and tag for an asset 70 | func (e *EKSD) ImageInfo(name string) (repo string, tag string, err error) { 71 | c := e.findComponent(name) 72 | if c == nil { 73 | // dns can be under the name coredns 74 | if strings.ToLower(name) == "dns" { 75 | return e.ImageInfo("coredns") 76 | } 77 | return "", "", fmt.Errorf("Component %s not found in release %v", name, e.release.Spec) 78 | } 79 | for _, a := range c.Assets { 80 | if a.Type == "Image" { 81 | i := strings.Split(a.Image.URI, ":") 82 | repo := i[0][:strings.LastIndex(i[0], "/")] 83 | if name == "Kubernetes" { 84 | firstDashIdx := strings.Index(i[1], "-") 85 | if firstDashIdx != -1 { 86 | return repo, i[1][firstDashIdx:], nil 87 | } 88 | } 89 | return repo, i[1], nil 90 | } 91 | } 92 | return "", "", fmt.Errorf("No image info for %s component in release %v", name, e.release.Spec) 93 | 94 | } 95 | 96 | // readRelease given a release URL this reads the yaml from the URL and converts to an EKS-D release 97 | func readRelease(releaseURL string) (*distrov1alpha1.Release, error) { 98 | res, err := http.Get(releaseURL) 99 | if err != nil { 100 | return nil, err 101 | } 102 | data, err := ioutil.ReadAll(res.Body) 103 | res.Body.Close() 104 | if err != nil { 105 | return nil, err 106 | } 107 | r := distrov1alpha1.Release{} 108 | err = yaml.Unmarshal([]byte(data), &r) 109 | if err != nil { 110 | return nil, err 111 | } 112 | return &r, nil 113 | } 114 | -------------------------------------------------------------------------------- /.circleci/config.yml: -------------------------------------------------------------------------------- 1 | version: 2 2 | 3 | # https://circleci.com/blog/circleci-hacks-reuse-yaml-in-your-circleci-config-with-yaml/ 4 | defaults: &defaults 5 | docker: 6 | - image: docker.io/weaveworks/wksctl-build:go-1.14.1-431fabe9 7 | environment: 8 | GOPATH: /go/ 9 | SRCDIR: /src/github.com/weaveworks/wksctl 10 | KUBECTL_URL: https://dl.k8s.io/v1.18.3/kubernetes-client-linux-amd64.tar.gz 11 | KUBECTL_CHECKSUM: 2096615904534a381d02ec15d62fe4e7fb80ef0d8e5fcfee2e71ba94771adfab 12 | working_directory: /home/circleci/src/github.com/weaveworks/cluster-api-provider-existinginfra 13 | 14 | integrationTestCommonEnv: &integrationTestCommonEnv 15 | SRCDIR: /src/github.com/weaveworks/wks 16 | KUBECTL_URL: https://dl.k8s.io/v1.18.3/kubernetes-client-linux-amd64.tar.gz 17 | KUBECTL_CHECKSUM: 2096615904534a381d02ec15d62fe4e7fb80ef0d8e5fcfee2e71ba94771adfab 18 | NAMESPACE: test 19 | CONTROL_PLANE_MACHINE_COUNT: 1 20 | WORKER_MACHINE_COUNT: 1 21 | KUBERNETES_VERSION: 1.17.7 22 | 23 | workflows: 24 | version: 2 25 | test-build-deploy: 26 | jobs: 27 | - build: 28 | filters: 29 | tags: 30 | only: /.*/ 31 | - unit-tests 32 | - integration-tests: 33 | requires: 34 | - build 35 | 36 | jobs: 37 | build: 38 | <<: *defaults 39 | steps: 40 | - checkout 41 | - setup_remote_docker 42 | - run: make 43 | - run: 44 | name: Check that generated files have not been changed since checkout 45 | command: | 46 | echo "The following files have been modified since checkout or are unknown to Git:" 47 | ! (git status --porcelain | grep -E '^( M)|(??)') 48 | - run: 49 | # Allow builds from forks 50 | name: Push image if docker login is available 51 | command: | 52 | if [ -n "$DOCKER_IO_PASSWORD" ]; then 53 | docker login -u "$DOCKER_IO_USER" -p "$DOCKER_IO_PASSWORD" docker.io 54 | make push 55 | fi 56 | unit-tests: 57 | <<: *defaults 58 | steps: 59 | - checkout 60 | - run: 61 | name: Install kubectl 62 | command: | 63 | curl -L $KUBECTL_URL -o kubectl.tar.gz 64 | echo "$KUBECTL_CHECKSUM kubectl.tar.gz" | sha256sum -c 65 | tar xvzf kubectl.tar.gz --strip-components=3 66 | sudo mv kubectl /usr/local/bin 67 | 68 | - run: 69 | name: Run unit tests 70 | command: | 71 | go version 72 | make test 73 | integration-tests: 74 | requires: 75 | - build 76 | machine: 77 | image: ubuntu-1604:202004-01 78 | environment: 79 | <<: *integrationTestCommonEnv 80 | working_directory: /home/circleci/src/github.com/weaveworks/cluster-api-provider-existinginfra 81 | steps: 82 | - checkout 83 | - run: 84 | name: Install kubectl 85 | command: | 86 | curl -L $KUBECTL_URL -o kubectl.tar.gz 87 | echo "$KUBECTL_CHECKSUM kubectl.tar.gz" | sha256sum -c 88 | tar xvzf kubectl.tar.gz --strip-components=3 89 | sudo mv kubectl /usr/local/bin 90 | - run: 91 | name: Run integration tests 92 | command: | 93 | IMAGE_TAG=$(./tools/image-tag) 94 | EXISTINGINFRA_CONTROLLER_IMAGE="weaveworks/cluster-api-existinginfra-controller:${IMAGE_TAG}" 95 | go version 96 | cd test/integration/test 97 | go test --timeout=99999s 98 | no_output_timeout: 30m 99 | 100 | -------------------------------------------------------------------------------- /apis/baremetalproviderspec/v1alpha1/clusterspec_types.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2020 Weaveworks. 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 | // +kubebuilder:object:root=true 24 | 25 | // ClusterSpec is the Schema for the clusterspecs API 26 | type ClusterSpec struct { 27 | metav1.TypeMeta `json:",inline"` 28 | // This ObjectMeta is not stored on encode, it is here just to provide 29 | // support for annotations that are required for comment preservation. 30 | metav1.ObjectMeta `json:"-"` 31 | 32 | User string `json:"user"` 33 | DeprecatedSSHKeyPath string `json:"sshKeyPath"` 34 | HTTPProxy string `json:"httpProxy,omitempty"` 35 | 36 | Authentication *AuthenticationWebhook `json:"authenticationWebhook,omitempty"` 37 | Authorization *AuthorizationWebhook `json:"authorizationWebhook,omitempty"` 38 | 39 | OS OSConfig `json:"os,omitempty"` 40 | CRI ContainerRuntime `json:"cri"` 41 | ImageRepository string `json:"imageRepository,omitempty"` 42 | 43 | APIServer APIServer `json:"apiServer,omitempty"` 44 | 45 | KubeletArguments []ServerArgument `json:"kubeletArguments,omitempty"` 46 | 47 | Addons []Addon `json:"addons,omitempty"` 48 | 49 | CloudProvider string `json:"cloudProvider,omitempty"` 50 | } 51 | 52 | type OSConfig struct { 53 | Files []FileSpec `json:"files,omitempty"` 54 | } 55 | 56 | type FileSpec struct { 57 | Source SourceSpec `json:"source"` 58 | Destination string `json:"destination"` 59 | } 60 | 61 | type SourceSpec struct { 62 | ConfigMap string `json:"configmap"` 63 | Key string `json:"key"` 64 | } 65 | 66 | type ContainerRuntime struct { 67 | Kind string `json:"kind"` 68 | Package string `json:"package"` 69 | Version string `json:"version"` 70 | } 71 | 72 | type APIServer struct { 73 | ExternalLoadBalancer string `json:"externalLoadBalancer"` 74 | AdditionalSANs []string `json:"additionalSANs,omitempty"` 75 | ExtraArguments []ServerArgument `json:"extraArguments,omitempty"` 76 | } 77 | 78 | type ServerArgument struct { 79 | Name string `json:"name"` 80 | Value string `json:"value"` 81 | } 82 | 83 | type AuthenticationWebhook struct { 84 | CacheTTL string `json:"cacheTTL,omitempty"` 85 | URL string `json:"url"` 86 | SecretFile string `json:"secretFile"` 87 | } 88 | 89 | type AuthorizationWebhook struct { 90 | CacheAuthorizedTTL string `json:"cacheAuthorizedTTL,omitempty"` 91 | CacheUnauthorizedTTL string `json:"cacheUnauthorizedTTL,omitempty"` 92 | URL string `json:"url"` 93 | SecretFile string `json:"secretFile"` 94 | } 95 | 96 | // Addon describes an addon to install on the cluster. 97 | type Addon struct { 98 | Name string `json:"name"` 99 | Params map[string]string `json:"params,omitempty"` 100 | Deps []string `json:"deps,omitempty"` 101 | } 102 | 103 | func init() { 104 | localSchemeBuilder.Register(addKnownClusterTypes) 105 | } 106 | -------------------------------------------------------------------------------- /pkg/plan/resource/kubectl_wait.go: -------------------------------------------------------------------------------- 1 | package resource 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | "strings" 7 | "time" 8 | 9 | "github.com/pkg/errors" 10 | "github.com/weaveworks/cluster-api-provider-existinginfra/pkg/plan" 11 | ) 12 | 13 | // KubectlWait waits for an object to reach a required state 14 | type KubectlWait struct { 15 | Base 16 | 17 | // Namespace specifies the namespace in which to search for the object being waited on 18 | WaitNamespace string `structs:"namespace"` 19 | // WaitType specifies the object type to wait for 20 | WaitType string `structs:"typeWaitedFor"` 21 | // WaitSelector, if not empty, specifies which instances of the type to wait for 22 | WaitSelector string `structs:"itemsWaitedFor"` 23 | // WaitCondition specifies the condition to wait for 24 | WaitCondition string `structs:"waitFor"` 25 | // WaitTimeout, if specified, indicates how long to wait for the WaitCondition to become true before failing (default 30s) 26 | WaitTimeout string `structs:"waitTimeout"` 27 | } 28 | 29 | var _ plan.Resource = plan.RegisterResource(&KubectlWait{}) 30 | 31 | // State implements plan.Resource. 32 | func (kw *KubectlWait) State() plan.State { 33 | return ToState(kw) 34 | } 35 | 36 | // Apply performs a "kubectl wait" as specified in the receiver. 37 | func (kw *KubectlWait) Apply(ctx context.Context, runner plan.Runner, diff plan.Diff) (bool, error) { 38 | if err := kubectlWait(ctx, runner, kubectlWaitArgs{ 39 | WaitNamespace: kw.WaitNamespace, 40 | WaitCondition: kw.WaitCondition, 41 | WaitType: kw.WaitType, 42 | WaitSelector: kw.WaitSelector, 43 | WaitTimeout: kw.WaitTimeout, 44 | }); err != nil { 45 | return false, err 46 | } 47 | 48 | return true, nil 49 | } 50 | 51 | type kubectlWaitArgs struct { 52 | // Namespace specifies the namespace in which to search for the object being waited on 53 | WaitNamespace string 54 | // WaitType specifies the object type to wait for 55 | WaitType string 56 | // WaitSelector, if non-empty, specifies the specific entities to "kubectl wait" on 57 | WaitSelector string 58 | // WaitCondition, if non-empty, makes kubectlWait do "kubectl wait --for=" on the applied resource. 59 | WaitCondition string 60 | // WaitTimeout, if specified, indicates how long to wait for the WaitCondition to become true before failing 61 | WaitTimeout string 62 | } 63 | 64 | func kubectlWait(ctx context.Context, r plan.Runner, args kubectlWaitArgs) error { 65 | // Assume the objects to wait for should/will exist. Don't start the timeout until they are present 66 | for { 67 | cmd := fmt.Sprintf("kubectl get %q %s%s", args.WaitType, waitOn(args), waitNamespace(args)) 68 | output, err := r.RunCommand(ctx, WithoutProxy(cmd), nil) 69 | if err != nil || strings.Contains(output, "No resources found") { 70 | time.Sleep(500 * time.Millisecond) 71 | } else { 72 | break 73 | } 74 | } 75 | cmd := fmt.Sprintf("kubectl wait %q --for=%q%s%s%s", 76 | args.WaitType, args.WaitCondition, waitOn(args), waitTimeout(args), waitNamespace(args)) 77 | if _, err := r.RunCommand(ctx, WithoutProxy(cmd), nil); err != nil { 78 | return errors.Wrap(err, "kubectl wait") 79 | } 80 | return nil 81 | } 82 | 83 | func waitOn(args kubectlWaitArgs) string { 84 | if args.WaitSelector != "" { 85 | return fmt.Sprintf(" --selector=%q", args.WaitSelector) 86 | } 87 | return "" 88 | } 89 | 90 | func waitTimeout(args kubectlWaitArgs) string { 91 | if args.WaitTimeout != "" { 92 | return fmt.Sprintf(" --timeout=%q", args.WaitTimeout) 93 | } 94 | return "" 95 | } 96 | 97 | func waitNamespace(args kubectlWaitArgs) string { 98 | if args.WaitNamespace != "" { 99 | return fmt.Sprintf(" --namespace=%q", args.WaitNamespace) 100 | } 101 | return "" 102 | } 103 | -------------------------------------------------------------------------------- /pkg/utilities/kubeadm/joincmd_test.go: -------------------------------------------------------------------------------- 1 | package kubeadm_test 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/stretchr/testify/assert" 7 | "github.com/weaveworks/cluster-api-provider-existinginfra/pkg/utilities/kubeadm" 8 | ) 9 | 10 | const ( 11 | kubeadmInitPartialStdout = `Your Kubernetes master has initialized successfully! 12 | 13 | To start using your cluster, you need to run the following as a regular user: 14 | 15 | mkdir -p $HOME/.kube 16 | sudo cp -i /etc/kubernetes/admin.conf $HOME/.kube/config 17 | sudo chown $(id -u):$(id -g) $HOME/.kube/config 18 | 19 | You should now deploy a pod network to the cluster. 20 | Run "kubectl apply -f [podnetwork].yaml" with one of the options listed at: 21 | https://kubernetes.io/docs/concepts/cluster-administration/addons/ 22 | 23 | You can now join any number of machines by running the following on each node 24 | as root: 25 | 26 | kubeadm join 172.17.0.2:6443 --token hpr1w5.ec293ztzcstptgz6 --discovery-token-ca-cert-hash sha256:ddddf4513d9dd1641b6da46cf5a83f18f269a72e1e7145d55c4cbc03fd4f7309 27 | 28 | ` 29 | kubeadmJoinCmd = "kubeadm join 172.17.0.2:6443 --token hpr1w5.ec293ztzcstptgz6 --discovery-token-ca-cert-hash sha256:ddddf4513d9dd1641b6da46cf5a83f18f269a72e1e7145d55c4cbc03fd4f7309" 30 | ) 31 | 32 | func TestExtractJoinCmd(t *testing.T) { 33 | extractedKubeadmJoinCmd, err := kubeadm.ExtractJoinCmd(kubeadmInitPartialStdout) 34 | assert.NoError(t, err) 35 | assert.Equal(t, kubeadmJoinCmd, extractedKubeadmJoinCmd) 36 | } 37 | 38 | func TestExtractDiscoveryTokenCaCertHash(t *testing.T) { 39 | hash, err := kubeadm.ExtractDiscoveryTokenCaCertHash(kubeadmJoinCmd) 40 | assert.NoError(t, err) 41 | assert.Equal(t, "sha256:ddddf4513d9dd1641b6da46cf5a83f18f269a72e1e7145d55c4cbc03fd4f7309", hash) 42 | } 43 | 44 | const ( 45 | kubeadmInitPartialStdoutWithMultilineJoin = `Your Kubernetes control-plane has initialized successfully! 46 | 47 | To start using your cluster, you need to run the following as a regular user: 48 | 49 | mkdir -p $HOME/.kube 50 | sudo cp -i /etc/kubernetes/admin.conf $HOME/.kube/config 51 | sudo chown $(id -u):$(id -g) $HOME/.kube/config 52 | 53 | You should now deploy a pod network to the cluster. 54 | Run "kubectl apply -f [podnetwork].yaml" with one of the options listed at: 55 | https://kubernetes.io/docs/concepts/cluster-administration/addons/ 56 | 57 | Then you can join any number of worker nodes by running the following on each as root: 58 | 59 | kubeadm join 172.17.0.2:6443 --token 2wbvbd.bib9jtbupd9vu7ke \ 60 | --discovery-token-ca-cert-hash sha256:c70a0fcbbce8e1b579b6af359daef91e3dd1a37ce1359c4ee726c441253503b2 61 | ` 62 | kubeadmJoinCmdOneLiner = "kubeadm join 172.17.0.2:6443 --token 2wbvbd.bib9jtbupd9vu7ke --discovery-token-ca-cert-hash sha256:c70a0fcbbce8e1b579b6af359daef91e3dd1a37ce1359c4ee726c441253503b2" 63 | ) 64 | 65 | func TestExtractJoinCmdLineContinuation(t *testing.T) { 66 | extractedKubeadmJoinCmd, err := kubeadm.ExtractJoinCmd(kubeadmInitPartialStdoutWithMultilineJoin) 67 | assert.NoError(t, err) 68 | assert.Equal(t, kubeadmJoinCmdOneLiner, extractedKubeadmJoinCmd) 69 | } 70 | 71 | func TestExtractCertificateKey(t *testing.T) { 72 | cmd := "kubeadm join 192.168.0.200:6443 --token 9vr73a.a8uxyaju799qwdjv --discovery-token-ca-cert-hash sha256:7c2e69131a36ae2a042a339b33381c6d0d43887e2de83720eff5359e26aec866 --experimental-control-plane" 73 | expectedKey := "f8902e114ef118304e561c3ecd4d0b543adc226b7a07f675f56564185ffe0c07" 74 | // case 1: --certificate-key 75 | key, err := kubeadm.ExtractCertificateKey(cmd + " --certificate-key " + expectedKey) 76 | assert.NoError(t, err) 77 | assert.Equal(t, expectedKey, key) 78 | // case 2: --certificate-key= 79 | key, err = kubeadm.ExtractCertificateKey(cmd + " --certificate-key=" + expectedKey) 80 | assert.NoError(t, err) 81 | assert.Equal(t, expectedKey, key) 82 | } 83 | -------------------------------------------------------------------------------- /pkg/apis/wksprovider/machine/config/kubeadm/kubeadm_test.go: -------------------------------------------------------------------------------- 1 | package kubeadm_test 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/stretchr/testify/assert" 7 | "github.com/weaveworks/cluster-api-provider-existinginfra/pkg/apis/wksprovider/machine/config" 8 | "github.com/weaveworks/cluster-api-provider-existinginfra/pkg/apis/wksprovider/machine/config/kubeadm" 9 | kubeadmapi "k8s.io/kubernetes/cmd/kubeadm/app/apis/kubeadm/v1beta1" 10 | "sigs.k8s.io/yaml" 11 | ) 12 | 13 | func TestSerializeKubeadmClusterConfiguration(t *testing.T) { 14 | cfg := kubeadm.NewClusterConfiguration(kubeadm.ClusterConfigurationParams{ 15 | KubernetesVersion: "x.y.z", 16 | NodeIPs: []string{"127.0.0.1", "1.2.3.4"}, 17 | }) 18 | assert.NotNil(t, cfg) 19 | bytes, err := yaml.Marshal(cfg) 20 | assert.NoError(t, err) 21 | yamlCfg := string(bytes) 22 | assert.Contains(t, yamlCfg, "kubernetesVersion: x.y.z") 23 | assert.Contains(t, yamlCfg, "apiVersion: kubeadm.k8s.io/v1beta1") 24 | assert.Contains(t, yamlCfg, "kind: ClusterConfiguration") 25 | assert.Contains(t, yamlCfg, `apiServer: 26 | certSANs: 27 | - localhost 28 | - 127.0.0.1 29 | - 1.2.3.4`) 30 | assert.Contains(t, yamlCfg, "controlPlaneEndpoint: localhost:6443") 31 | } 32 | 33 | func TestSerializeKubeadmInitConfiguration(t *testing.T) { 34 | cfg := kubeadm.NewInitConfiguration(kubeadm.InitConfigurationParams{ 35 | KubeletConfig: config.KubeletConfig{NodeIP: "127.0.0.1"}, 36 | BootstrapToken: &kubeadmapi.BootstrapTokenString{ 37 | ID: "abcdef", 38 | Secret: "abcdefghijklmnop", 39 | }, 40 | }) 41 | assert.NotNil(t, cfg) 42 | bytes, err := yaml.Marshal(cfg) 43 | assert.NoError(t, err) 44 | assert.Equal(t, `apiVersion: kubeadm.k8s.io/v1beta1 45 | bootstrapTokens: 46 | - token: abcdef.abcdefghijklmnop 47 | kind: InitConfiguration 48 | localAPIEndpoint: 49 | advertiseAddress: 127.0.0.1 50 | bindPort: 0 51 | nodeRegistration: 52 | kubeletExtraArgs: 53 | node-ip: 127.0.0.1 54 | `, string(bytes)) 55 | } 56 | 57 | func TestSerializeKubeadmJoinConfiguration(t *testing.T) { 58 | cfg := kubeadm.NewJoinConfiguration(kubeadm.JoinConfigurationParams{ 59 | NodeIP: "127.0.0.1", 60 | APIServerEndpoint: "127.0.0.2:6443", 61 | Token: "t0k3n", 62 | CACertHash: "sha256:c3rth4sh", 63 | }) 64 | assert.NotNil(t, cfg) 65 | bytes, err := yaml.Marshal(cfg) 66 | assert.NoError(t, err) 67 | assert.Equal(t, `apiVersion: kubeadm.k8s.io/v1beta1 68 | caCertPath: "" 69 | discovery: 70 | bootstrapToken: 71 | apiServerEndpoint: 127.0.0.2:6443 72 | caCertHashes: 73 | - sha256:c3rth4sh 74 | token: t0k3n 75 | unsafeSkipCAVerification: false 76 | tlsBootstrapToken: "" 77 | kind: JoinConfiguration 78 | nodeRegistration: 79 | kubeletExtraArgs: 80 | node-ip: 127.0.0.1 81 | `, string(bytes)) 82 | } 83 | 84 | func TestAWSCloudProviderClusterConfig(t *testing.T) { 85 | cfg := kubeadm.NewClusterConfiguration(kubeadm.ClusterConfigurationParams{ 86 | KubernetesVersion: "x.y.z", 87 | NodeIPs: []string{"127.0.0.1", "1.2.3.4"}, 88 | CloudProvider: "aws", 89 | }) 90 | assert.NotNil(t, cfg) 91 | bytes, err := yaml.Marshal(cfg) 92 | assert.NoError(t, err) 93 | yamlCfg := string(bytes) 94 | assert.Contains(t, yamlCfg, "cloud-provider: aws") 95 | } 96 | 97 | func TestAWSKubletConfig(t *testing.T) { 98 | cfg := kubeadm.NewInitConfiguration(kubeadm.InitConfigurationParams{ 99 | KubeletConfig: config.KubeletConfig{NodeIP: "127.0.0.1", CloudProvider: "aws"}, 100 | BootstrapToken: &kubeadmapi.BootstrapTokenString{ 101 | ID: "abcdef", 102 | Secret: "abcdefghijklmnop", 103 | }, 104 | }) 105 | assert.NotNil(t, cfg) 106 | bytes, err := yaml.Marshal(cfg) 107 | assert.NoError(t, err) 108 | assert.Equal(t, `apiVersion: kubeadm.k8s.io/v1beta1 109 | bootstrapTokens: 110 | - token: abcdef.abcdefghijklmnop 111 | kind: InitConfiguration 112 | localAPIEndpoint: 113 | advertiseAddress: 127.0.0.1 114 | bindPort: 0 115 | nodeRegistration: 116 | kubeletExtraArgs: 117 | cloud-provider: aws 118 | node-ip: 127.0.0.1 119 | `, string(bytes)) 120 | } 121 | -------------------------------------------------------------------------------- /pkg/plan/resource/kube_secret.go: -------------------------------------------------------------------------------- 1 | package resource 2 | 3 | import ( 4 | "context" 5 | "crypto/sha256" 6 | "fmt" 7 | "path/filepath" 8 | "sort" 9 | "strings" 10 | 11 | "github.com/weaveworks/cluster-api-provider-existinginfra/pkg/plan" 12 | ) 13 | 14 | // KubeSecret writes secrets to the filesystem where they can be picked up by daemons 15 | type KubeSecret struct { 16 | Base 17 | 18 | // SecretName is the name of the secret to read 19 | SecretName string `structs:"secretName"` 20 | // Checksum contains the sha256 checksum of the secret data 21 | Checksum [sha256.Size]byte `structs:"checksum"` 22 | // DestinationDirectory is the location in which to write stored file data 23 | DestinationDirectory string `structs:"destinationDirectory"` 24 | // SecretData holds the actual secret contents -- not serialized 25 | SecretData SecretData `structs:"-" plan:"hide"` 26 | // FileNameTransform transforms a secret key into the file name for its contents 27 | FileNameTransform func(string) string 28 | } 29 | 30 | // SecretData maps names to values as in Kubernetes v1.Secret 31 | type SecretData map[string][]byte 32 | 33 | var ( 34 | _ plan.Resource = plan.RegisterResource(&KubeSecret{}) 35 | ) 36 | 37 | func flattenMap(m map[string][]byte) []byte { 38 | items := []string{} 39 | for key, val := range m { 40 | items = append(items, fmt.Sprintf("%s:%v", key, val)) 41 | } 42 | sort.Strings(items) 43 | return []byte(strings.Join(items, ",")) 44 | } 45 | 46 | // NewKubeSecretResource creates a new object from secret data 47 | func NewKubeSecretResource(secretName string, secretData SecretData, destinationDirectory string, fileNameTransform func(string) string) (*KubeSecret, error) { 48 | return &KubeSecret{ 49 | SecretName: secretName, 50 | Checksum: sha256.Sum256(flattenMap(secretData)), 51 | DestinationDirectory: destinationDirectory, 52 | SecretData: secretData, 53 | FileNameTransform: fileNameTransform, 54 | }, nil 55 | } 56 | 57 | // State implements plan.Resource. 58 | func (ks *KubeSecret) State() plan.State { 59 | return plan.State(map[string]interface{}{"checksum": ks.Checksum}) 60 | } 61 | 62 | func (ks *KubeSecret) QueryState(ctx context.Context, runner plan.Runner) (plan.State, error) { 63 | data := ks.SecretData 64 | for fname := range data { 65 | path := filepath.Join(ks.DestinationDirectory, ks.FileNameTransform(fname)) 66 | exists, err := fileExists(ctx, runner, path) 67 | if err != nil { 68 | return nil, err 69 | } 70 | if !exists { 71 | return plan.EmptyState, nil 72 | } 73 | contents, err := runner.RunCommand(ctx, fmt.Sprintf("cat %s", path), nil) 74 | if err != nil { 75 | return nil, err 76 | } 77 | data[fname] = []byte(contents) 78 | } 79 | return plan.State(map[string]interface{}{"checksum": sha256.Sum256(flattenMap(data))}), nil 80 | } 81 | 82 | func fileExists(ctx context.Context, runner plan.Runner, path string) (bool, error) { 83 | result, err := runner.RunCommand(ctx, fmt.Sprintf("[ -f %s ] && echo 'yes' || true", path), nil) 84 | if err != nil { 85 | return false, err 86 | } 87 | return result == "yes", nil 88 | } 89 | 90 | // Apply implements plan.Resource. 91 | func (ks *KubeSecret) Apply(ctx context.Context, runner plan.Runner, diff plan.Diff) (bool, error) { 92 | for fname, contents := range ks.SecretData { 93 | err := WriteFile(ctx, contents, filepath.Join(ks.DestinationDirectory, ks.FileNameTransform(fname)), 0600, runner) 94 | if err != nil { 95 | return false, err 96 | } 97 | } 98 | return true, nil 99 | } 100 | 101 | // Undo implements plan.Resource. 102 | func (ks *KubeSecret) Undo(ctx context.Context, runner plan.Runner, current plan.State) error { 103 | if len(ks.SecretData) == 0 { 104 | return nil 105 | } 106 | var sb strings.Builder 107 | sb.WriteString("{") 108 | shouldWriteComma := false 109 | for filename := range ks.SecretData { 110 | if shouldWriteComma { 111 | sb.WriteString(",") 112 | } else { 113 | shouldWriteComma = true 114 | } 115 | sb.WriteString(ks.FileNameTransform(filename)) 116 | } 117 | sb.WriteString("}") 118 | _, err := runner.RunCommand(ctx, fmt.Sprintf("rm -f %s/%s", ks.DestinationDirectory, sb.String()), nil) 119 | return err 120 | } 121 | -------------------------------------------------------------------------------- /pkg/plan/resource/service.go: -------------------------------------------------------------------------------- 1 | package resource 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | "strings" 7 | 8 | "github.com/pkg/errors" 9 | "github.com/weaveworks/cluster-api-provider-existinginfra/pkg/plan" 10 | ) 11 | 12 | const ( 13 | // ServiceInactive is a non-started service. 14 | ServiceInactive = "inactive" 15 | // ServiceActivating is a starting service. 16 | ServiceActivating = "activating" 17 | // ServiceActive is a started service. 18 | ServiceActive = "active" 19 | // ServiceFailed is a service that failed to start 20 | ServiceFailed = "failed" 21 | ) 22 | 23 | // Service represents a systemd service. 24 | type Service struct { 25 | // Name of the systemd unit. 26 | Name string `structs:"name"` 27 | // Status is the desired service status. Only "active" or "inactive" are valid 28 | // input. 29 | Status string `structs:"status"` 30 | // Whether the service is enabled (systemctl enable) or not. 31 | Enabled bool `structs:"enabled"` 32 | } 33 | 34 | var _ plan.Resource = plan.RegisterResource(&Service{}) 35 | 36 | // State implements plan.Resource. 37 | func (p *Service) State() plan.State { 38 | return ToState(p) 39 | } 40 | 41 | func systemd(ctx context.Context, r plan.Runner, format string, args ...interface{}) (string, error) { 42 | return r.RunCommand(ctx, fmt.Sprintf("systemctl "+format, args...), nil) 43 | } 44 | 45 | // QueryState implements plan.Resource. 46 | func (p *Service) QueryState(ctx context.Context, r plan.Runner) (plan.State, error) { 47 | // See https://bugzilla.redhat.com/show_bug.cgi?id=1073481#c11 48 | output, err := systemd(ctx, r, "show %s -p ActiveState", p.Name) 49 | if err != nil { 50 | return plan.EmptyState, err 51 | } 52 | status := keyval(output, "ActiveState") 53 | if status == "" { 54 | return plan.EmptyState, errors.Wrapf(err, "service %s: query: could not query active state", p.Name) 55 | } 56 | 57 | output, err = systemd(ctx, r, "is-enabled %s", p.Name) 58 | // is-enabled exits with non-zero status when the unit is disabled. 59 | if err != nil && line(output) != "disabled" { 60 | return plan.EmptyState, errors.Wrapf(err, "service %s: query: could not query enabled state", p.Name) 61 | } 62 | enabled := line(output) == "enabled" 63 | 64 | service := Service{ 65 | Name: p.Name, 66 | Status: status, 67 | Enabled: enabled, 68 | } 69 | return service.State(), nil 70 | } 71 | 72 | // Apply implements plan.Resource. 73 | func (p *Service) Apply(ctx context.Context, r plan.Runner, diff plan.Diff) (bool, error) { 74 | var err error 75 | var output string 76 | 77 | current := diff.CurrentState 78 | 79 | // Enabled 80 | if !current.Bool("enabled") && p.Enabled { 81 | output, err = systemd(ctx, r, "enable %s", p.Name) 82 | } else if current.Bool("enabled") && !p.Enabled { 83 | output, err = systemd(ctx, r, "disable %s", p.Name) 84 | } 85 | if err != nil { 86 | return false, fmt.Errorf("%s: %s", output, err.Error()) 87 | } 88 | 89 | // Active 90 | // XXX: We need to think about what happens when a unit is in failed status 91 | // (current["status"] is "failed"). 92 | if p.Status == ServiceActive && current.String("status") == ServiceInactive { 93 | output, err = systemd(ctx, r, "start %s", p.Name) 94 | } else if p.Status == ServiceInactive && current.String("status") != ServiceInactive { 95 | output, err = systemd(ctx, r, "stop %s", p.Name) 96 | } 97 | if err != nil { 98 | return false, fmt.Errorf("%s: %s", output, err.Error()) 99 | } 100 | 101 | return true, nil 102 | } 103 | 104 | // Undo implements plan.Resource 105 | func (p *Service) Undo(ctx context.Context, r plan.Runner, current plan.State) error { 106 | if current.Bool("enabled") { 107 | output, err := systemd(ctx, r, "disable %s", p.Name) 108 | if err != nil { 109 | if strings.Contains(output, "not loaded") { 110 | return nil 111 | } 112 | return fmt.Errorf("%s: %s", output, err.Error()) 113 | } 114 | } 115 | 116 | if current.String("status") != ServiceInactive { 117 | output, err := systemd(ctx, r, "stop %s", p.Name) 118 | if err != nil { 119 | if strings.Contains(output, "not loaded") { 120 | return nil 121 | } 122 | return fmt.Errorf("%s: %s", output, err.Error()) 123 | } 124 | } 125 | 126 | return nil 127 | } 128 | -------------------------------------------------------------------------------- /pkg/kubernetes/drain/facade.go: -------------------------------------------------------------------------------- 1 | package drain 2 | 3 | import ( 4 | "fmt" 5 | "time" 6 | 7 | "github.com/pkg/errors" 8 | log "github.com/sirupsen/logrus" 9 | corev1 "k8s.io/api/core/v1" 10 | "k8s.io/client-go/kubernetes" 11 | "k8s.io/kubectl/pkg/drain" 12 | ) 13 | 14 | // DefaultTimeOut is the default duration Drain will wait for pods to be 15 | // evicted before erroring. This value will be used if Params.TimeOut is not 16 | // provided. 17 | var DefaultTimeOut = 1 * time.Minute 18 | 19 | // Params groups required inputs to drain a Kubernetes node. 20 | type Params struct { 21 | Force bool 22 | DeleteEmptyDirData bool 23 | IgnoreAllDaemonSets bool 24 | TimeOut time.Duration 25 | } 26 | 27 | // Drain drains the provided node. 28 | func Drain(node *corev1.Node, clientSet kubernetes.Interface, params Params) error { 29 | drainLog := log.StandardLogger().Writer() 30 | defer drainLog.Close() 31 | drainer := &drain.Helper{ 32 | Client: clientSet, 33 | GracePeriodSeconds: -1, 34 | Force: params.Force, 35 | DeleteEmptyDirData: params.DeleteEmptyDirData, 36 | IgnoreAllDaemonSets: params.IgnoreAllDaemonSets, 37 | Out: drainLog, 38 | } 39 | policyGroupVersion, err := drain.CheckEvictionSupport(clientSet) 40 | if err != nil { 41 | return errors.Wrapf(err, "eviction not supported") 42 | } else if len(policyGroupVersion) == 0 { 43 | return fmt.Errorf("policy group version not found in the API server; eviction is not supported") 44 | } 45 | 46 | if err := cordon(node, clientSet); err != nil { 47 | return err 48 | } 49 | return evictPods(node, getOrDefault(params.TimeOut), drainer) 50 | } 51 | 52 | func cordon(node *corev1.Node, clientSet kubernetes.Interface) error { 53 | cordonHelper := drain.NewCordonHelper(node) 54 | // If the desired state (that the node should be unschedulable) doesn't match the actual state, 55 | // then do the patch and replace logic 56 | if cordonHelper.UpdateIfRequired(true) { 57 | // false means no server dry run logic shall take place 58 | err, patchErr := cordonHelper.PatchOrReplace(clientSet, false) 59 | if patchErr != nil { 60 | log.Warn(patchErr.Error()) 61 | } 62 | if err != nil { 63 | log.Error(err.Error()) 64 | } 65 | log.Infof("cordoning node %q", node.Name) 66 | } else { 67 | log.Debugf("no need to cordon node %q", node.Name) 68 | } 69 | return nil 70 | } 71 | 72 | func getOrDefault(timeOut time.Duration) time.Duration { 73 | if timeOut == 0 { 74 | return DefaultTimeOut 75 | } 76 | return timeOut 77 | } 78 | 79 | func evictPods(node *corev1.Node, timeOut time.Duration, drainer *drain.Helper) error { 80 | timer := time.After(timeOut) 81 | start := time.Now() 82 | for { 83 | select { 84 | case <-timer: 85 | return fmt.Errorf("timed out (after %s) waiting for node %q to be drain", timeOut, node.Name) 86 | default: 87 | numPendingPods, err := evictPodsOn(node, drainer) 88 | if err != nil { 89 | return err 90 | } 91 | log.Debugf("%d pod(s) to be evicted from %s", numPendingPods, node.Name) 92 | if numPendingPods == 0 { 93 | return nil 94 | } 95 | // Wait a bit, to avoid hitting the API server in a tight loop: 96 | wait(timeOut, start) 97 | } 98 | } 99 | } 100 | 101 | func evictPodsOn(node *corev1.Node, drainer *drain.Helper) (int, error) { 102 | podsForDeletion, errs := drainer.GetPodsForDeletion(node.Name) 103 | if len(errs) > 0 { 104 | return -1, fmt.Errorf("errors: %v", errs) // TODO: improve formatting 105 | } 106 | if w := podsForDeletion.Warnings(); w != "" { 107 | log.Warn(w) 108 | } 109 | pods := podsForDeletion.Pods() 110 | numPendingPods := len(pods) 111 | 112 | err := drainer.DeleteOrEvictPods(pods) 113 | return numPendingPods, err 114 | } 115 | 116 | func wait(timeOut time.Duration, start time.Time) { 117 | elapsed := time.Since(start) 118 | remainingTime := timeOut - elapsed 119 | time.Sleep(min(remainingTime, max(5*time.Second, timeOut/10))) 120 | } 121 | 122 | func max(x time.Duration, y time.Duration) time.Duration { 123 | if x < y { 124 | return y 125 | } 126 | return x 127 | } 128 | 129 | func min(x time.Duration, y time.Duration) time.Duration { 130 | if x < y { 131 | return x 132 | } 133 | return y 134 | } 135 | -------------------------------------------------------------------------------- /pkg/plan/resource/file.go: -------------------------------------------------------------------------------- 1 | package resource 2 | 3 | import ( 4 | "bytes" 5 | "context" 6 | "crypto/md5" 7 | "fmt" 8 | "io" 9 | "io/ioutil" 10 | "os" 11 | "strings" 12 | 13 | "github.com/pkg/errors" 14 | "github.com/weaveworks/cluster-api-provider-existinginfra/pkg/plan" 15 | ) 16 | 17 | // XXX: Expose file permission (if needed?) 18 | 19 | // File represents a file on the file system. 20 | type File struct { 21 | // Source is a path to a local file. Only of of (Source, Content) can be 22 | // specified at once. 23 | Source string `structs:"source,omitempty"` 24 | // Content is the file content. Only of of (Source, Content) can be specified 25 | // at once. 26 | Content string `structs:"content,omitempty"` 27 | // Destination is the file destination path (required). 28 | Destination string `structs:"destination"` 29 | // File MD5 checksum. We use md5sum as it's part of coreutils and even part of 30 | // the default alpine image. 31 | Checksum string `structs:"checksum" plan:"hide"` 32 | } 33 | 34 | var _ plan.Resource = plan.RegisterResource(&File{}) 35 | 36 | // State implements plan.Resource. 37 | func (f *File) State() plan.State { 38 | return ToState(f) 39 | } 40 | 41 | // QueryState implements plan.Resource. 42 | func (f *File) QueryState(ctx context.Context, runner plan.Runner) (plan.State, error) { 43 | output, err := runner.RunCommand(ctx, fmt.Sprintf("md5sum %s", f.Destination), nil) 44 | // XXX: this error message is actually locale dependent! 45 | if err != nil && strings.Contains(output, "No such file or directory") { 46 | return plan.EmptyState, nil 47 | } 48 | if err != nil { 49 | return plan.EmptyState, fmt.Errorf("query file %s failed: %v -- %s", f.Destination, err, output) 50 | } 51 | fields := strings.Fields(line(output)) 52 | state := f.State() 53 | state["checksum"] = fmt.Sprintf("md5:%s", fields[0]) 54 | return state, nil 55 | } 56 | 57 | func checksumFromFile(path string) ([]byte, error) { 58 | f, err := os.Open(path) 59 | if err != nil { 60 | return nil, err 61 | } 62 | defer f.Close() 63 | 64 | h := md5.New() 65 | if _, err := io.Copy(h, f); err != nil { 66 | return nil, err 67 | } 68 | 69 | return h.Sum(nil), nil 70 | } 71 | 72 | func checksumFromString(content string) []byte { 73 | h := md5.New() 74 | _, _ = io.WriteString(h, content) 75 | return h.Sum(nil) 76 | } 77 | 78 | func (f *File) computeChecksum() error { 79 | if f.Checksum != "" { 80 | return nil 81 | } 82 | 83 | var sum []byte 84 | var err error 85 | if f.Source != "" { 86 | sum, err = checksumFromFile(f.Source) 87 | } else { 88 | sum = checksumFromString(f.Content) 89 | } 90 | if err != nil { 91 | return err 92 | } 93 | f.Checksum = fmt.Sprintf("md5:%x", sum) 94 | return nil 95 | } 96 | 97 | func (f *File) content() ([]byte, error) { 98 | if f.Source != "" { 99 | return ioutil.ReadFile(f.Source) 100 | } 101 | return []byte(f.Content), nil 102 | } 103 | 104 | // Apply implements plan.Resource. 105 | func (f *File) Apply(ctx context.Context, runner plan.Runner, diff plan.Diff) (bool, error) { 106 | if err := f.computeChecksum(); err != nil { 107 | return false, errors.Wrapf(err, "file: %s", f.Destination) 108 | } 109 | 110 | if f.State().Equal(diff.CurrentState) { 111 | return false, nil 112 | } 113 | 114 | content, err := f.content() 115 | if err != nil { 116 | return false, err 117 | } 118 | 119 | return true, WriteFile(ctx, content, f.Destination, 0660, runner) 120 | } 121 | 122 | // Undo implements plan.Resource. 123 | func (f *File) Undo(ctx context.Context, runner plan.Runner, current plan.State) error { 124 | // Not checking checksum on Undo since File resources are being 125 | // used to undo actions taken within commands like kubeadminit. 126 | // In some cases we need to make sure files that would have been 127 | // created by the command are gone but we don't know if they've been 128 | // created or not. 129 | _, err := runner.RunCommand(ctx, fmt.Sprintf("rm -f %s", f.Destination), nil) 130 | return err 131 | } 132 | 133 | func WriteFile(ctx context.Context, content []byte, dstPath string, perm os.FileMode, runner plan.Runner) error { 134 | input := bytes.NewReader(content) 135 | cmd := fmt.Sprintf("mkdir -pv $(dirname %q) && sed -n 'w %s' && chmod 0%o %q", dstPath, dstPath, perm, dstPath) 136 | _, err := runner.RunCommand(ctx, cmd, input) 137 | return err 138 | } 139 | -------------------------------------------------------------------------------- /pkg/plan/resource/kubeadm_join.go: -------------------------------------------------------------------------------- 1 | package resource 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | "strings" 7 | 8 | "github.com/pkg/errors" 9 | log "github.com/sirupsen/logrus" 10 | "github.com/weaveworks/cluster-api-provider-existinginfra/pkg/plan" 11 | "github.com/weaveworks/cluster-api-provider-existinginfra/pkg/utilities/version" 12 | ) 13 | 14 | // KubeadmJoin represents an attempt to join a Kubernetes node via kubeadm. 15 | type KubeadmJoin struct { 16 | Base 17 | 18 | // IsMaster should be true if this node should join as a master, or false otherwise. 19 | IsMaster bool `structs:"isMaster"` 20 | // NodeIP is the IP of the node trying to join the cluster. 21 | NodeIP string `structs:"nodeIP"` 22 | // NodeName, if non-empty, will override the default node name guessed by kubeadm. 23 | NodeName string 24 | // MasterIP is the IP of the master node to connect to in order to join the cluster -- 25 | // hidden because the value can change in multi-master configurations but should not make the node plan 26 | // appear to have changed. 27 | MasterIP string `structs:"masterIP" plan:"hide"` 28 | // MasterPort is the port of the master node to connect to in order to join the cluster. 29 | MasterPort int `structs:"masterPort"` 30 | // Token is used to authenticate with the Kubernetes API server. 31 | Token string `structs:"token" plan:"hide"` 32 | // DiscoveryTokenCaCertHash is used to validate that the root CA public key of the cluster we are trying to join matches. 33 | DiscoveryTokenCaCertHash string `structs:"discoveryTokenCaCertHash" plan:"hide"` 34 | // CertificateKey is used to add master nodes to the cluster. 35 | CertificateKey string `structs:"certificateKey" plan:"hide"` 36 | // IgnorePreflightErrors is optionally used to skip kubeadm's preflight checks. 37 | IgnorePreflightErrors []string `structs:"ignorePreflightErrors"` 38 | // External Load Balancer name or IP address to be used instead of the master's IP 39 | ControlPlaneEndpoint string `structs:"controlPlaneEndpoint"` 40 | // Kubernetes Version is used to prepare different parameters 41 | KubernetesVersion string `structs:"version"` 42 | } 43 | 44 | var _ plan.Resource = plan.RegisterResource(&KubeadmJoin{}) 45 | 46 | // State implements plan.Resource. 47 | func (kj *KubeadmJoin) State() plan.State { 48 | return ToState(kj) 49 | } 50 | 51 | // Apply implements plan.Resource. 52 | // TODO: find a way to make this idempotent. 53 | // TODO: should such a resource be splitted in smaller resources? 54 | func (kj *KubeadmJoin) Apply(ctx context.Context, runner plan.Runner, diff plan.Diff) (bool, error) { 55 | log.Info("joining Kubernetes cluster") 56 | apiServerEndpoint := fmt.Sprintf("%s:%d", kj.MasterIP, kj.MasterPort) 57 | if kj.ControlPlaneEndpoint != "" { 58 | apiServerEndpoint = fmt.Sprintf("%s:%d", kj.ControlPlaneEndpoint, kj.MasterPort) 59 | } 60 | kubeadmJoinCmd := kj.kubeadmJoinCmd(apiServerEndpoint) 61 | if stdouterr, err := runner.RunCommand(ctx, WithoutProxy(kubeadmJoinCmd), nil); err != nil { 62 | log.WithField("stdouterr", stdouterr).Error("failed to join cluster") 63 | return false, errors.Wrap(err, "failed to join cluster") 64 | } 65 | return true, nil 66 | } 67 | 68 | func (kj *KubeadmJoin) kubeadmJoinCmd(apiServerEndpoint string) string { 69 | var kubeJoinCmd strings.Builder 70 | kubeJoinCmd.WriteString("kubeadm join") 71 | if len(kj.IgnorePreflightErrors) > 0 { 72 | kubeJoinCmd.WriteString(" --ignore-preflight-errors=") 73 | kubeJoinCmd.WriteString(strings.Join(kj.IgnorePreflightErrors, ",")) 74 | } 75 | 76 | if kj.IsMaster { 77 | if lt, err := version.LessThan(kj.KubernetesVersion, "1.16.0"); err == nil && lt { 78 | kubeJoinCmd.WriteString(" --experimental-control-plane --certificate-key ") 79 | } else { 80 | kubeJoinCmd.WriteString(" --control-plane --certificate-key ") 81 | } 82 | 83 | kubeJoinCmd.WriteString(kj.CertificateKey) 84 | } 85 | kubeJoinCmd.WriteString(" --node-name=") 86 | kubeJoinCmd.WriteString(kj.NodeName) 87 | kubeJoinCmd.WriteString(" --token ") 88 | kubeJoinCmd.WriteString(kj.Token) 89 | kubeJoinCmd.WriteString(" --discovery-token-ca-cert-hash ") 90 | kubeJoinCmd.WriteString(kj.DiscoveryTokenCaCertHash) 91 | kubeJoinCmd.WriteString(" ") 92 | kubeJoinCmd.WriteString(apiServerEndpoint) 93 | return kubeJoinCmd.String() 94 | } 95 | 96 | // Undo implements plan.Resource. 97 | func (kj *KubeadmJoin) Undo(ctx context.Context, runner plan.Runner, current plan.State) error { 98 | return errors.New("not implemented") 99 | } 100 | -------------------------------------------------------------------------------- /pkg/plan/recipe/upgrade_plans.go: -------------------------------------------------------------------------------- 1 | package recipe 2 | 3 | import ( 4 | "fmt" 5 | 6 | "github.com/weaveworks/cluster-api-provider-existinginfra/pkg/plan" 7 | "github.com/weaveworks/cluster-api-provider-existinginfra/pkg/plan/resource" 8 | "github.com/weaveworks/cluster-api-provider-existinginfra/pkg/utilities/object" 9 | "github.com/weaveworks/cluster-api-provider-existinginfra/pkg/utilities/version" 10 | ) 11 | 12 | type NodeType int 13 | 14 | const ( 15 | OriginalMaster NodeType = iota 16 | SecondaryMaster 17 | Worker 18 | ) 19 | 20 | // BuildUpgradePlan creates a sub-plan to run upgrade using respective package management commands. 21 | func BuildUpgradePlan(pkgType resource.PkgType, k8sVersion string, ntype NodeType) (plan.Resource, error) { 22 | b := plan.NewBuilder() 23 | 24 | // install new packages - kubelet and kubectl need to be installed before kubeadm 25 | switch pkgType { 26 | case resource.PkgTypeRPM, resource.PkgTypeRHEL: 27 | b.AddResource( 28 | "upgrade:node-unlock-kubernetes", 29 | &resource.Run{Script: object.String("yum versionlock delete 'kube*' || true")}) 30 | b.AddResource( 31 | "upgrade:node-kubelet", 32 | &resource.RPM{Name: "kubelet", Version: k8sVersion, DisableExcludes: "kubernetes"}, 33 | plan.DependOn("upgrade:node-unlock-kubernetes")) 34 | b.AddResource( 35 | "upgrade:node-kubectl", 36 | &resource.RPM{Name: "kubectl", Version: k8sVersion, DisableExcludes: "kubernetes"}, 37 | plan.DependOn("upgrade:node-kubelet")) 38 | b.AddResource( 39 | "upgrade:node-install-kubeadm", 40 | &resource.RPM{Name: "kubeadm", Version: k8sVersion, DisableExcludes: "kubernetes"}, 41 | plan.DependOn("upgrade:node-kubectl")) 42 | b.AddResource( 43 | "upgrade:node-lock-kubernetes", 44 | &resource.Run{Script: object.String("yum versionlock add 'kube*' || true")}, 45 | plan.DependOn("upgrade:node-install-kubeadm")) 46 | case resource.PkgTypeDeb: 47 | b.AddResource( 48 | "upgrade:node-unlock-kubernetes", 49 | &resource.Run{Script: object.String("apt-mark unhold 'kube*' || true")}) 50 | b.AddResource( 51 | "upgrade:node-kubelet", 52 | &resource.Deb{Name: "kubelet", Suffix: "=" + k8sVersion + "-00"}, 53 | plan.DependOn("upgrade:node-unlock-kubernetes")) 54 | b.AddResource( 55 | "upgrade:node-kubectl", 56 | &resource.Deb{Name: "kubectl", Suffix: "=" + k8sVersion + "-00"}, 57 | plan.DependOn("upgrade:node-kubelet")) 58 | b.AddResource( 59 | "upgrade:node-install-kubeadm", 60 | &resource.Deb{Name: "kubeadm", Suffix: "=" + k8sVersion + "-00"}, 61 | plan.DependOn("upgrade:node-kubectl")) 62 | b.AddResource( 63 | "upgrade:node-lock-kubernetes", 64 | &resource.Run{Script: object.String("apt-mark hold 'kube*' || true")}, 65 | plan.DependOn("upgrade:node-install-kubeadm")) 66 | } 67 | // 68 | // For secondary masters 69 | // version >= 1.16.0 uses: kubeadm upgrade node 70 | // version >= 1.14.0 && < 1.16.0 uses: kubeadm upgrade node experimental-control-plane 71 | // 72 | secondaryMasterUpgradeControlPlaneFlag := "" 73 | if lt, err := version.LessThan(k8sVersion, "v1.16.0"); err == nil && lt { 74 | secondaryMasterUpgradeControlPlaneFlag = "experimental-control-plane" 75 | } 76 | 77 | switch ntype { 78 | case OriginalMaster: 79 | b.AddResource( 80 | "upgrade:node-kubeadm-upgrade", 81 | &resource.Run{Script: object.String(fmt.Sprintf("kubeadm upgrade plan && kubeadm upgrade apply -y %s", k8sVersion))}, 82 | plan.DependOn("upgrade:node-install-kubeadm")) 83 | case SecondaryMaster: 84 | b.AddResource( 85 | "upgrade:node-kubeadm-upgrade", 86 | &resource.Run{Script: object.String(fmt.Sprintf("kubeadm upgrade node %s", secondaryMasterUpgradeControlPlaneFlag))}, 87 | plan.DependOn("upgrade:node-install-kubeadm")) 88 | case Worker: 89 | b.AddResource( 90 | "upgrade:node-kubeadm-upgrade", 91 | // From kubeadm upgrade node phase kubelet-config --help 92 | // > kubeadm uses the KuberneteVersion field in the kubeadm-config ConfigMap to determine what the _desired_ kubelet version is. 93 | &resource.Run{Script: object.String("kubeadm upgrade node phase kubelet-config")}, 94 | plan.DependOn("upgrade:node-install-kubeadm")) 95 | } 96 | 97 | switch pkgType { 98 | case resource.PkgTypeRPM, resource.PkgTypeRHEL: 99 | b.AddResource( 100 | "upgrade:node-restart-kubelet", 101 | &resource.Run{Script: object.String("systemctl restart kubelet")}, 102 | plan.DependOn("upgrade:node-kubeadm-upgrade")) 103 | case resource.PkgTypeDeb: 104 | b.AddResource( 105 | "upgrade:node-restart-kubelet", 106 | &resource.Run{Script: object.String("systemctl restart kubelet")}, 107 | plan.DependOn("upgrade:node-kubeadm-upgrade")) 108 | } 109 | 110 | p, err := b.Plan() 111 | if err != nil { 112 | return nil, err 113 | } 114 | return &p, nil 115 | } 116 | -------------------------------------------------------------------------------- /pkg/plan/resource/rpm_test.go: -------------------------------------------------------------------------------- 1 | package resource 2 | 3 | import ( 4 | "context" 5 | "testing" 6 | 7 | "github.com/stretchr/testify/assert" 8 | "github.com/weaveworks/cluster-api-provider-existinginfra/pkg/plan" 9 | "github.com/weaveworks/cluster-api-provider-existinginfra/pkg/plan/runners/sudo" 10 | ) 11 | 12 | func makeRPMState(name, version, release string) plan.State { 13 | return map[string]interface{}{ 14 | "name": name, 15 | "version": version, 16 | "release": release, 17 | } 18 | } 19 | 20 | func TestRPMStateDifferent(t *testing.T) { 21 | makeNoVersion := &RPM{ 22 | Name: "make", 23 | } 24 | makeVersion := &RPM{ 25 | Name: "make", 26 | Version: "3.82", 27 | } 28 | makeRelease := &RPM{ 29 | Name: "make", 30 | Version: "3.82", 31 | Release: "23.el7", 32 | } 33 | 34 | tests := []struct { 35 | p *RPM 36 | current plan.State 37 | expected bool 38 | }{ 39 | {makeNoVersion, plan.EmptyState, true}, 40 | {makeVersion, plan.EmptyState, true}, 41 | {makeVersion, plan.EmptyState, true}, 42 | {makeRelease, plan.EmptyState, true}, 43 | 44 | // make already installed with a compatible (version, release) 45 | {makeNoVersion, makeRPMState("make", "3.82", "23.el7"), false}, 46 | {makeVersion, makeRPMState("make", "3.82", "23.el7"), false}, 47 | {makeRelease, makeRPMState("make", "3.82", "23.el7"), false}, 48 | 49 | // make already installed but with an incompatible version or release. 50 | {makeVersion, makeRPMState("make", "3.83", "01.el7"), true}, 51 | {makeRelease, makeRPMState("make", "3.82", "24.el7"), true}, 52 | } 53 | 54 | for _, test := range tests { 55 | assert.Equal(t, test.expected, test.p.stateDifferent(test.current)) 56 | } 57 | } 58 | 59 | func TestRevisionComparison(t *testing.T) { 60 | makeNoVersion := &RPM{ 61 | Name: "make", 62 | } 63 | makeCurrentVersion := &RPM{ 64 | Name: "make", 65 | Version: "3.82", 66 | } 67 | makeRelease := &RPM{ 68 | Name: "make", 69 | Version: "3.82", 70 | Release: "23.el7", 71 | } 72 | makeNewVersion := &RPM{ 73 | Name: "make", 74 | Version: "3.83", 75 | } 76 | makeOldVersion := &RPM{ 77 | Name: "make", 78 | Version: "3.81", 79 | } 80 | tests := []struct { 81 | p1 *RPM 82 | p2 *RPM 83 | expected bool 84 | }{ 85 | {makeNoVersion, makeOldVersion, true}, 86 | {makeNoVersion, makeCurrentVersion, true}, 87 | {makeNoVersion, makeRelease, true}, 88 | {makeNoVersion, makeNewVersion, true}, 89 | 90 | {makeOldVersion, makeNoVersion, false}, 91 | {makeCurrentVersion, makeNoVersion, false}, 92 | {makeRelease, makeNoVersion, false}, 93 | {makeNewVersion, makeNoVersion, false}, 94 | 95 | {makeOldVersion, makeCurrentVersion, true}, 96 | {makeOldVersion, makeRelease, true}, 97 | {makeOldVersion, makeNewVersion, true}, 98 | 99 | {makeOldVersion, makeOldVersion, false}, 100 | {makeCurrentVersion, makeOldVersion, false}, 101 | {makeRelease, makeOldVersion, false}, 102 | {makeNewVersion, makeOldVersion, false}, 103 | 104 | {makeCurrentVersion, makeRelease, true}, 105 | {makeCurrentVersion, makeNewVersion, true}, 106 | 107 | {makeCurrentVersion, makeCurrentVersion, false}, 108 | {makeRelease, makeCurrentVersion, false}, 109 | {makeNewVersion, makeCurrentVersion, false}, 110 | 111 | {makeRelease, makeNewVersion, true}, 112 | 113 | {makeRelease, makeRelease, false}, 114 | {makeNewVersion, makeRelease, false}, 115 | 116 | {makeNewVersion, makeNewVersion, false}, 117 | } 118 | 119 | for _, test := range tests { 120 | assert.Equal(t, test.expected, lowerRevisionThan(test.p1.State(), test.p2.State())) 121 | } 122 | } 123 | 124 | func TestUndo(t *testing.T) { 125 | ctx := context.Background() 126 | // Test that we perform an Undo when passed an empty state 127 | undid := false 128 | undoAction = func(_ context.Context, _ *RPM, _ plan.Runner, _ plan.State, _ string) error { 129 | undid = true 130 | return nil 131 | } 132 | res := &RPM{Name: "make", Version: "3.82", Release: "23.el7"} 133 | err := res.Undo(ctx, &sudo.Runner{}, plan.EmptyState) 134 | assert.NoError(t, err) 135 | assert.True(t, undid) 136 | 137 | // Test that we can choose to remove ANY version 138 | var description string 139 | undoAction = func(_ context.Context, _ *RPM, _ plan.Runner, _ plan.State, pkgDesc string) error { 140 | description = pkgDesc 141 | return nil 142 | } 143 | err = res.Undo(ctx, &sudo.Runner{}, plan.EmptyState) 144 | assert.NoError(t, err) 145 | assert.Equal(t, description, "make") 146 | 147 | // Test that we can choose to remove only the matching version 148 | res = &RPM{Name: "make", Version: "3.82", Release: "23.el7", IgnoreOtherVersions: true} 149 | err = res.Undo(ctx, &sudo.Runner{}, plan.EmptyState) 150 | assert.NoError(t, err) 151 | assert.Equal(t, description, "make-3.82-23.el7") 152 | } 153 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | VERSION=$(shell git describe --always --match "v*") 2 | IMAGE_TAG := $(shell tools/image-tag) 3 | # Produce CRDs that work back to Kubernetes 1.11 (no version conversion) 4 | CRD_OPTIONS ?= "crd" 5 | 6 | API_ROOT := ./apis 7 | API_DIRS := ${API_ROOT}/baremetalproviderspec/v1alpha1,${API_ROOT}/cluster.weave.works/v1alpha3 8 | 9 | GOOS=$(shell go env GOOS) 10 | GOARCH=$(shell go env GOARCH) 11 | 12 | export KUBEBUILDER_ASSETS=$(shell pwd)/bin/kubebuilder 13 | 14 | # Get the currently used golang install path (in GOPATH/bin, unless GOBIN is set) 15 | ifeq (,$(shell go env GOBIN)) 16 | GOBIN=$(shell go env GOPATH)/bin 17 | else 18 | GOBIN=$(shell go env GOBIN) 19 | endif 20 | 21 | all: manager 22 | 23 | # Run tests 24 | unit-tests: generate fmt vet manifests manager $(KUBEBUILDER_ASSETS) 25 | CGO_ENABLED=0 go test -v ./pkg/... ./controllers/... -coverprofile cover.out -covermode=atomic 26 | 27 | # Generate CRDs 28 | CRDS=$(shell find config/crd -name '*.yaml' -print) 29 | pkg/apis/wksprovider/machine/crds/crds_vfsdata.go: $(CRDS) 30 | go generate ./pkg/apis/wksprovider/machine/crds 31 | 32 | # Generate Manifests 33 | MANIFESTS=$(shell find pkg/apis/wksprovider/manifests/yaml -name '*.yaml' -print) 34 | pkg/apis/wksprovider/manifests/manifests_vfsdata.go: $(MANIFESTS) 35 | go generate ./pkg/apis/wksprovider/manifests 36 | 37 | # Build manager binary 38 | manager: pkg/apis/wksprovider/machine/crds/crds_vfsdata.go pkg/apis/wksprovider/manifests/manifests_vfsdata.go generate fmt vet 39 | CGO_ENABLED=0 go build -ldflags "-X github.com/weaveworks/cluster-api-provider-existinginfra/pkg/utilities/version.Version=$(VERSION)" -o bin/manager main.go 40 | 41 | # Run against the configured Kubernetes cluster in ~/.kube/config 42 | run: generate fmt vet manifests 43 | go run ./main.go 44 | 45 | # Install CRDs into a cluster 46 | install: manifests 47 | kustomize build config/crd | kubectl apply -f - 48 | 49 | # Uninstall CRDs from a cluster 50 | uninstall: manifests 51 | kustomize build config/crd | kubectl delete -f - 52 | 53 | # Clean up images and binaries 54 | clean: 55 | rm -f bin/manager 56 | docker rmi -f docker.io/weaveworks/cluster-api-existinginfra-controller:${IMAGE_TAG} 57 | 58 | # Deploy controller in the configured Kubernetes cluster in ~/.kube/config 59 | deploy: manifests 60 | cd config/manager && kustomize edit set image controller=docker.io/weaveworks/cluster-api-existinginfra-controller:${IMAGE_TAG} 61 | kustomize build config/default | kubectl apply -f - 62 | 63 | # Generate manifests e.g. CRD, RBAC etc. 64 | manifests: controller-gen 65 | $(CONTROLLER_GEN) $(CRD_OPTIONS) rbac:roleName=manager-role webhook paths="./..." output:crd:artifacts:config=config/crd/bases 66 | 67 | # Run go fmt against code 68 | fmt: 69 | go fmt ./... 70 | 71 | # Run go vet against code 72 | vet: 73 | go vet ./... 74 | 75 | # Generate code 76 | generate: controller-gen conversion-gen image-tag-gen 77 | $(CONTROLLER_GEN) object:headerFile="hack/boilerplate.go.txt" paths="./..." 78 | $(CONVERSION_GEN) \ 79 | --output-base ../../. \ 80 | --input-dirs ${API_DIRS} \ 81 | -O zz_generated.conversion \ 82 | -h hack/boilerplate.go.txt 83 | 84 | # Build the docker image 85 | docker-build: unit-tests 86 | docker build . -t weaveworks/cluster-api-existinginfra-controller:${IMAGE_TAG} 87 | 88 | # Push the docker image 89 | push: docker-build 90 | docker push weaveworks/cluster-api-existinginfra-controller:${IMAGE_TAG} 91 | 92 | # Generate code containing an image manifest that tracks the current IMAGE_TAG so 93 | # this code can be used upstream by builds that don't have access to the IMAGE_TAG 94 | image-tag-gen: 95 | @cp templates/image_tag.template pkg/utilities/version/generated.go 96 | @echo "\"$(IMAGE_TAG)\"" >> pkg/utilities/version/generated.go 97 | 98 | # find or download controller-gen 99 | # download controller-gen if necessary 100 | controller-gen: 101 | ifeq (, $(shell which controller-gen)) 102 | @{ \ 103 | set -e ;\ 104 | CONTROLLER_GEN_TMP_DIR=$$(mktemp -d) ;\ 105 | cd $$CONTROLLER_GEN_TMP_DIR ;\ 106 | go mod init tmp ;\ 107 | go get sigs.k8s.io/controller-tools/cmd/controller-gen@v0.3.0 ;\ 108 | rm -rf $$CONTROLLER_GEN_TMP_DIR ;\ 109 | } 110 | CONTROLLER_GEN=$(GOBIN)/controller-gen 111 | else 112 | CONTROLLER_GEN=$(shell which controller-gen) 113 | endif 114 | 115 | conversion-gen: 116 | ifeq (, $(shell which conversion-gen)) 117 | @{ \ 118 | set -e ;\ 119 | CONVERSION_GEN_TMP_DIR=$$(mktemp -d) ;\ 120 | cd $$CONVERSION_GEN_TMP_DIR ;\ 121 | go mod init tmp ;\ 122 | go get k8s.io/code-generator/cmd/conversion-gen ;\ 123 | rm -rf $$CONVERSION_GEN_TMP_DIR ;\ 124 | } 125 | CONVERSION_GEN=$(GOBIN)/conversion-gen 126 | else 127 | CONVERSION_GEN=$(shell which conversion-gen) 128 | endif 129 | 130 | $(KUBEBUILDER_ASSETS): 131 | mkdir -p $@ 132 | curl -sSL https://go.kubebuilder.io/dl/2.3.1/$(GOOS)/$(GOARCH) | tar -xz --strip-components=2 -C $@ 133 | -------------------------------------------------------------------------------- /pkg/plan/state_test.go: -------------------------------------------------------------------------------- 1 | package plan 2 | 3 | import ( 4 | "fmt" 5 | "log" 6 | "strings" 7 | "testing" 8 | 9 | "github.com/stretchr/testify/assert" 10 | ) 11 | 12 | func p(s string) State { 13 | r := strings.NewReader(s) 14 | params, err := NewStateFromJSON(r) 15 | if err != nil { 16 | log.Fatal(s, err) 17 | } 18 | return params 19 | } 20 | 21 | func TestGet(t *testing.T) { 22 | tests := []struct { 23 | o State 24 | path string 25 | valid bool 26 | expected interface{} 27 | }{ 28 | {map[string]interface{}{}, "foo.bar", false, nil}, 29 | {p(`{ "foo": 2 }`), "foo.bar", false, nil}, 30 | {p(`{ "foo": { "bar": 2 } }`), "foo.bar", true, float64(2)}, 31 | {p(`{ "foo": { "bar": "baz" } }`), "foo.bar", true, "baz"}, 32 | {p(`{ "foo": { "bar": { "baz": 3 } } }`), "foo.bar", true, p(`{ "baz": 3 }`)}, 33 | {p(`{ "xxx": "yyy", "foo": { "bar": { "baz": 3 } } }`), "foo.bar", true, p(`{ "baz": 3 }`)}, 34 | } 35 | 36 | for _, test := range tests { 37 | v, err := test.o.Get(test.path) 38 | if test.valid { 39 | assert.NoError(t, err) 40 | } else { 41 | assert.Error(t, err) 42 | } 43 | assert.Equal(t, test.expected, v) 44 | } 45 | } 46 | 47 | func TestTypedGet(t *testing.T) { 48 | params := p(`{ "xxx": "yyy", "foo": { "bar": { "baz": 3 }, "boolean": true } }`) 49 | 50 | vBool, err := params.GetBool("foo.boolean") 51 | assert.NoError(t, err) 52 | assert.Equal(t, true, vBool) 53 | 54 | vNumber, err := params.GetNumber("foo.bar.baz") 55 | assert.NoError(t, err) 56 | assert.Equal(t, float64(3), vNumber) 57 | 58 | vString, err := params.GetString("xxx") 59 | assert.NoError(t, err) 60 | assert.Equal(t, "yyy", vString) 61 | _, err = params.GetString("foo.bar.baz") 62 | assert.Error(t, err) 63 | 64 | vObject, err := params.GetObject("foo.bar") 65 | assert.NoError(t, err) 66 | assert.Equal(t, p(`{ "baz": 3 }`), vObject) 67 | 68 | } 69 | 70 | func TestCoercion(t *testing.T) { 71 | params := p(`{ "n": "0.2", "b": "true" }`) 72 | 73 | // Happy cases. 74 | vNumber, err := params.GetNumber("n") 75 | assert.NoError(t, err) 76 | assert.Equal(t, 0.2, vNumber) 77 | 78 | vBool, err := params.GetBool("b") 79 | assert.NoError(t, err) 80 | assert.Equal(t, true, vBool) 81 | 82 | // Invalid coercion. 83 | _, err = params.GetNumber("b") 84 | assert.Error(t, err) 85 | _, err = params.GetBool("n") 86 | assert.Error(t, err) 87 | } 88 | 89 | func TestSet(t *testing.T) { 90 | tests := []struct { 91 | o State 92 | path string 93 | value interface{} 94 | expected State 95 | }{ 96 | {p(`{}`), "foo", float64(2), p(`{ "foo": 2 }`)}, 97 | {p(`{}`), "foo", "bar", p(`{ "foo": "bar" }`)}, 98 | {p(`{}`), "foo", true, p(`{ "foo": true }`)}, 99 | {p(`{}`), "foo", p(`{ "bar": "baz" } `), p(`{ "foo": { "bar": "baz" } }`)}, 100 | {p(`{ "foo": { "xxx": 42 } }`), "foo.yyy", p(`{ "bar": "baz" } `), p(`{ "foo": { "xxx": 42, "yyy": { "bar": "baz" } } }`)}, 101 | } 102 | 103 | for _, test := range tests { 104 | test.o.Set(test.path, test.value) 105 | assert.Equal(t, test.expected, test.o) 106 | } 107 | } 108 | 109 | func TestMerge(t *testing.T) { 110 | tests := []struct { 111 | a, b State 112 | expected State 113 | }{ 114 | {NewState(), NewState(), NewState()}, 115 | {map[string]interface{}{}, map[string]interface{}{}, map[string]interface{}{}}, 116 | {map[string]interface{}{}, map[string]interface{}{"foo": 1}, map[string]interface{}{"foo": 1}}, 117 | {map[string]interface{}{}, map[string]interface{}{"foo": "bar"}, map[string]interface{}{"foo": "bar"}}, 118 | {map[string]interface{}{"foo": 1}, map[string]interface{}{}, map[string]interface{}{"foo": 1}}, 119 | {map[string]interface{}{"foo": "bar"}, map[string]interface{}{}, map[string]interface{}{"foo": "bar"}}, 120 | 121 | {p(`{ "foo": 1 } `), p(`{ "foo": { "bar": "baz" } }`), p(`{"foo": { "bar": "baz" } }`)}, 122 | {p(`{ "foo": 1, "orig": "xxx" } `), p(`{ "foo": { "bar": "baz" } }`), p(`{"foo": { "bar": "baz" }, "orig": "xxx" }`)}, 123 | {p(`{ "foo": { "rab": "zab" }, "orig": "xxx" } `), p(`{ "foo": { "bar": "baz" } }`), p(`{"foo": { "bar": "baz", "rab": "zab" }, "orig": "xxx" }`)}, 124 | {p(`{ "foo": { "bar": "baz" } }`), p(`{ "foo": { "rab": "zab" }, "orig": "xxx" } `), p(`{"foo": { "bar": "baz", "rab": "zab" }, "orig": "xxx" }`)}, 125 | } 126 | 127 | for _, test := range tests { 128 | test.a.Merge(test.b) 129 | assert.Equal(t, test.expected, test.a) 130 | } 131 | 132 | } 133 | 134 | func TestEqual(t *testing.T) { 135 | tests := []struct { 136 | json1 string 137 | json2 string 138 | expected bool 139 | }{ 140 | {`{}`, `{}`, true}, 141 | {`{ }`, `{ "foo": 2 }`, false}, 142 | // Data types 143 | {`{ "foo": 2 }`, `{ "foo": "2" }`, false}, 144 | {`{ "foo": "2" }`, `{ "foo": "2" }`, true}, 145 | // Check whitespace differences don't matter 146 | {`{ "foo": 2 }`, `{ "foo": 2 }`, true}, 147 | {`{ "foo": 2 }`, `{ 148 | "foo": 2 }`, true}, 149 | // Check ordering doesn't matter 150 | {`{ "foo": 1, "bar": 2 }`, `{"bar": 2, "foo": 1 }`, true}, 151 | } 152 | 153 | for i, test := range tests { 154 | t.Run(fmt.Sprintf("%d", i), func(t *testing.T) { 155 | state1 := p(test.json1) 156 | state2 := p(test.json2) 157 | assert.Equal(t, test.expected, state1.Equal(state2), fmt.Sprintf("%q == %q", test.json1, test.json2)) 158 | }) 159 | } 160 | } 161 | -------------------------------------------------------------------------------- /pkg/plan/resource/rpm.go: -------------------------------------------------------------------------------- 1 | package resource 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | "strings" 7 | 8 | "github.com/cavaliercoder/go-rpm/version" 9 | "github.com/weaveworks/cluster-api-provider-existinginfra/pkg/plan" 10 | ) 11 | 12 | // RPM represents an RPM package. 13 | // 14 | // It isn't legal to provide a Release if no Version is specified. 15 | // TODO: What about epoch? 16 | type RPM struct { 17 | Name string `structs:"name"` 18 | // Version is optional 19 | Version string `structs:"version,omitempty"` 20 | Release string `structs:"release,omitempty"` 21 | IgnoreOtherVersions bool `structs:"ignoreOtherVersions,omitempty"` 22 | DisableExcludes string `structs:"disableExcludes,omitempty"` 23 | } 24 | 25 | type rpmState plan.State 26 | 27 | // Name implements version.Interface 28 | func (s rpmState) Name() string { 29 | if name, ok := s["name"]; ok { 30 | return name.(string) 31 | } 32 | return "" 33 | } 34 | 35 | // Epoch implements version.Interface 36 | func (s rpmState) Epoch() int { 37 | return 0 38 | } 39 | 40 | // Version implements version.Interface 41 | func (s rpmState) Version() string { 42 | if version, ok := s["version"]; ok { 43 | return version.(string) 44 | } 45 | return "" 46 | } 47 | 48 | // Release implements version.Interface 49 | func (s rpmState) Release() string { 50 | if release, ok := s["release"]; ok { 51 | return release.(string) 52 | } 53 | return "" 54 | } 55 | 56 | var _ plan.Resource = plan.RegisterResource(&RPM{}) 57 | 58 | // State implements plan.Resource. 59 | func (p *RPM) State() plan.State { 60 | return ToState(p) 61 | } 62 | 63 | func lowerRevisionThan(state1, state2 plan.State) bool { 64 | return version.Compare(rpmState(state1), rpmState(state2)) < 0 65 | } 66 | 67 | func label(name, version, release string) string { 68 | if release != "" { 69 | return fmt.Sprintf("%s-%s-%s", name, version, release) 70 | } 71 | if version != "" { 72 | return fmt.Sprintf("%s-%s", name, version) 73 | } 74 | return name 75 | 76 | } 77 | 78 | func (p *RPM) label() string { 79 | return label(p.Name, p.Version, p.Release) 80 | } 81 | 82 | // QueryState implements plan.Resource. 83 | func (p *RPM) QueryState(ctx context.Context, r plan.Runner) (plan.State, error) { 84 | output, err := r.RunCommand(ctx, fmt.Sprintf("rpm -q --queryformat '%%{NAME} %%{VERSION} %%{RELEASE}\\n' %s", p.label()), nil) 85 | if err != nil && strings.Contains(output, "is not installed") { 86 | // Package isn't installed. 87 | return plan.EmptyState, nil 88 | } 89 | if err != nil { 90 | // An error happened running rpm. 91 | return plan.EmptyState, fmt.Errorf("query rpm %s failed: %v -- %s", p.label(), err, output) 92 | } 93 | 94 | // XXX: in theory rpm queries can return multiple versions of the same package 95 | // if all of them are installed a the same. This shouldn't be a thing for the 96 | // packages we query. 97 | l := line(output) 98 | parts := strings.Split(l, " ") 99 | queriedPackage := &RPM{ 100 | Name: parts[0], 101 | Version: parts[1], 102 | Release: parts[2], 103 | } 104 | return queriedPackage.State(), nil 105 | } 106 | 107 | func (p *RPM) stateDifferent(current plan.State) bool { 108 | if current.IsEmpty() { 109 | return true 110 | } 111 | 112 | desired := p.label() 113 | installed := label(current.String("name"), current.String("version"), current.String("release")) 114 | return !strings.HasPrefix(installed, desired) 115 | } 116 | 117 | // WouldChangeState returns false if a call to Apply() is guaranteed not to change the installed version of the package, and true otherwise. 118 | func (p *RPM) WouldChangeState(ctx context.Context, r plan.Runner) (bool, error) { 119 | current, err := p.QueryState(ctx, r) 120 | if err != nil { 121 | return false, err 122 | } 123 | return p.stateDifferent(current), nil 124 | } 125 | 126 | // Apply implements plan.Resource. 127 | func (p *RPM) Apply(ctx context.Context, r plan.Runner, diff plan.Diff) (bool, error) { 128 | if !p.stateDifferent(diff.CurrentState) { 129 | return false, nil 130 | } 131 | 132 | // First assume the package doesn't exist at all 133 | var cmd string 134 | switch { 135 | case diff.CurrentState.IsEmpty(): 136 | cmd = fmt.Sprintf("yum -y install %s", p.label()) 137 | case lowerRevisionThan(diff.CurrentState, p.State()): 138 | cmd = fmt.Sprintf("yum -y upgrade-to %s", p.label()) 139 | case lowerRevisionThan(p.State(), diff.CurrentState): 140 | cmd = fmt.Sprintf("yum -y remove %s && yum -y install %s", p.Name, p.label()) 141 | } 142 | 143 | if p.DisableExcludes != "" { 144 | cmd = fmt.Sprintf("%s --disableexcludes %s", cmd, p.DisableExcludes) 145 | } 146 | _, err := r.RunCommand(ctx, cmd, nil) 147 | return err == nil, err 148 | } 149 | 150 | // Separate the action out so that it can be mocked 151 | var undoAction = func(ctx context.Context, p *RPM, r plan.Runner, current plan.State, pkgDescription string) error { 152 | _, err := r.RunCommand(ctx, fmt.Sprintf("yum -y remove %s || true", pkgDescription), nil) 153 | return err 154 | } 155 | 156 | // Undo implements plan.Resource 157 | func (p *RPM) Undo(ctx context.Context, r plan.Runner, current plan.State) error { 158 | pkgDescription := p.Name 159 | if p.IgnoreOtherVersions { 160 | pkgDescription = p.label() 161 | } 162 | return undoAction(ctx, p, r, current, pkgDescription) 163 | } 164 | -------------------------------------------------------------------------------- /pkg/plan/runners/ssh/ssh.go: -------------------------------------------------------------------------------- 1 | package ssh 2 | 3 | import ( 4 | "bytes" 5 | "context" 6 | "fmt" 7 | "io" 8 | "os" 9 | 10 | "github.com/pkg/errors" 11 | log "github.com/sirupsen/logrus" 12 | "github.com/weaveworks/cluster-api-provider-existinginfra/pkg/plan" 13 | sshutil "github.com/weaveworks/cluster-api-provider-existinginfra/pkg/utilities/ssh" 14 | "golang.org/x/crypto/ssh" 15 | ) 16 | 17 | // ClientParams groups inputs to build a client object. 18 | type ClientParams struct { 19 | User string 20 | Host string 21 | Port uint16 22 | PrivateKeyPath string 23 | PrivateKey []byte 24 | PrintOutputs bool 25 | } 26 | 27 | // Client is a higher-level abstraction around the standard API's SSH 28 | // configuration, client and connection to the remote machine. 29 | type Client struct { 30 | client *ssh.Client 31 | printOutputs bool 32 | } 33 | 34 | var _ plan.Runner = &Client{} 35 | 36 | const tcp = "tcp" 37 | 38 | // NewClient instantiates a new SSH Client object. 39 | // N.B.: provide either the key (privateKey) or its path (privateKeyPath). 40 | func NewClient(params ClientParams) (*Client, error) { 41 | log.WithFields(log.Fields{"user": params.User, "host": params.Host, "port": params.Port, "privateKeyPath": params.PrivateKeyPath, "printOutputs": params.PrintOutputs}).Infof("creating SSH client") 42 | signer, err := sshutil.SignerFromPrivateKey(params.PrivateKeyPath, params.PrivateKey) 43 | if err != nil { 44 | return nil, errors.Wrapf(err, "failed to read private key from \"%s\"", params.PrivateKeyPath) 45 | } 46 | hostPublicKey, err := sshutil.HostPublicKey(params.Host) 47 | if err != nil { 48 | return nil, errors.Wrapf(err, "failed to read host %s's public key", params.Host) 49 | } 50 | config := &ssh.ClientConfig{ 51 | User: params.User, 52 | Auth: []ssh.AuthMethod{ 53 | ssh.PublicKeys(signer), 54 | }, 55 | HostKeyCallback: sshutil.HostKeyCallback(hostPublicKey), 56 | } 57 | hostPort := fmt.Sprintf("%s:%d", params.Host, params.Port) 58 | client, err := ssh.Dial(tcp, hostPort, config) 59 | if err != nil { 60 | return nil, errors.Wrapf(err, 61 | "failed to connect to %s using private key %s as user %s, please verify connection manually", hostPort, params.PrivateKeyPath, config.User) 62 | } 63 | return &Client{ 64 | client: client, 65 | printOutputs: params.PrintOutputs, 66 | }, nil 67 | } 68 | 69 | // RunCommand executes the provided command on the remote machine configured in 70 | // this Client object. A new Session is created for each call to RunCommand. 71 | // A Client supports multiple interactive sessions. 72 | func (c *Client) RunCommand(ctx context.Context, command string, stdin io.Reader) (string, error) { 73 | log.Debugf("running command: %s", command) 74 | return c.handleSessionIO(func(session *ssh.Session) error { 75 | session.Stdin = stdin 76 | return session.Start(command) 77 | }) 78 | } 79 | 80 | // Handle output and command completion for a remote shell 81 | func (c *Client) handleSessionIO(action func(*ssh.Session) error) (string, error) { 82 | session, err := c.client.NewSession() 83 | if err != nil { 84 | return "", errors.Wrap(err, "failed to create new SSH session") 85 | } 86 | defer session.Close() 87 | // Write stdout and stderr to both this process' stdout and stderr, and 88 | // buffers, for later re-use. 89 | stdOutPipe, err := session.StdoutPipe() 90 | if err != nil { 91 | return "", errors.Wrap(err, "failed to get pipe to standard output") 92 | } 93 | stdErrPipe, err := session.StderrPipe() 94 | if err != nil { 95 | return "", errors.Wrap(err, "failed to get pipe to standard error") 96 | } 97 | var stdOutErr bytes.Buffer 98 | outWriters := []io.Writer{&stdOutErr} 99 | errWriters := []io.Writer{&stdOutErr} 100 | if c.printOutputs { 101 | outWriters = append(outWriters, os.Stdout) 102 | errWriters = append(errWriters, os.Stderr) 103 | } 104 | stdOutWriter := io.MultiWriter(outWriters...) 105 | stdErrWriter := io.MultiWriter(errWriters...) 106 | 107 | err = action(session) 108 | 109 | // Don't respond to err until output complete 110 | var errStdOut, errStdErr error 111 | syncChan := make(chan bool) 112 | go func() { 113 | _, errStdOut = io.Copy(stdOutWriter, stdOutPipe) 114 | syncChan <- true 115 | }() 116 | go func() { 117 | _, errStdErr = io.Copy(stdErrWriter, stdErrPipe) 118 | syncChan <- true 119 | }() 120 | 121 | // Make sure copying is finished 122 | <-syncChan 123 | <-syncChan 124 | 125 | // Now we can return the error 126 | if err != nil { 127 | return stdOutErr.String(), errors.Wrap(err, "failed while remote executing") 128 | } 129 | 130 | if err := session.Wait(); err != nil { 131 | if err, ok := err.(*ssh.ExitError); ok { 132 | return stdOutErr.String(), &plan.RunError{ExitCode: err.ExitStatus()} 133 | } 134 | return stdOutErr.String(), errors.Wrap(err, "failed while waiting for end of remote execution") 135 | } 136 | 137 | if errStdOut != nil { 138 | return stdOutErr.String(), errors.Wrap(errStdOut, "failed while capturing stdout") 139 | } 140 | if errStdErr != nil { 141 | return stdOutErr.String(), errors.Wrap(errStdErr, "failed while capturing stderr") 142 | } 143 | return stdOutErr.String(), nil 144 | } 145 | 146 | // Close closes this high-level Client's underlying SSH connection. 147 | func (c *Client) Close() error { 148 | return c.client.Close() 149 | } 150 | --------------------------------------------------------------------------------