├── .gitignore ├── Dockerfile ├── LICENSE ├── Makefile ├── PROJECT ├── README.md ├── api └── v1beta1 │ ├── crdrelease_types.go │ ├── groupversion_info.go │ ├── source_types.go │ └── zz_generated.deepcopy.go ├── clm-server.yaml ├── config ├── certmanager │ ├── certificate.yaml │ ├── kustomization.yaml │ └── kustomizeconfig.yaml ├── crd │ ├── bases │ │ ├── clm.cloudnativeapp.io_crdreleases.yaml │ │ └── clm.cloudnativeapp.io_sources.yaml │ ├── kustomization.yaml │ ├── kustomizeconfig.yaml │ └── patches │ │ ├── cainjection_in_crdreleases.yaml │ │ ├── cainjection_in_sources.yaml │ │ ├── webhook_in_crdreleases.yaml │ │ └── webhook_in_sources.yaml ├── default │ ├── kustomization.yaml │ ├── manager_auth_proxy_patch.yaml │ ├── manager_webhook_patch.yaml │ └── webhookcainjection_patch.yaml ├── manager │ ├── kustomization.yaml │ └── manager.yaml ├── prometheus │ ├── kustomization.yaml │ └── monitor.yaml ├── rbac │ ├── auth_proxy_client_clusterrole.yaml │ ├── auth_proxy_role.yaml │ ├── auth_proxy_role_binding.yaml │ ├── auth_proxy_service.yaml │ ├── crdrelease_editor_role.yaml │ ├── crdrelease_viewer_role.yaml │ ├── kustomization.yaml │ ├── leader_election_role.yaml │ ├── leader_election_role_binding.yaml │ ├── role.yaml │ ├── role_binding.yaml │ ├── source_editor_role.yaml │ └── source_viewer_role.yaml ├── sample │ ├── crdrelease │ │ ├── helm-example.yaml │ │ ├── helm-repo-example.yaml │ │ └── native-example.yaml │ ├── import │ │ ├── helm-import-example.yaml │ │ ├── native-import-example.yaml │ │ └── nginx-deployment.yaml │ └── source │ │ ├── helm-source.yaml │ │ └── native-source.yaml └── webhook │ ├── kustomization.yaml │ ├── kustomizeconfig.yaml │ └── service.yaml ├── controllers ├── crdrelease_controller.go ├── crdrelease_svc.go ├── source_controller.go └── suite_test.go ├── docs ├── crdrelease.md ├── helm-source.md ├── native-source.md └── service-source.md ├── go.mod ├── go.sum ├── hack └── boilerplate.go.txt ├── images ├── architecture.jpg ├── crd.jpg ├── crdreleases.jpg ├── helm.jpg ├── module-source.jpg └── nginx-deploy.jpg ├── internal ├── crdrelease_svc.go ├── dependency_svc.go ├── module_svc.go ├── readiness_svc.go ├── registry_svc.go ├── source_svc.go └── zz_generated.deepcopy.go ├── main.go └── pkg ├── check ├── condition │ ├── condition.go │ └── zz_generated.deepcopy.go ├── precheck │ ├── crdcheck.go │ └── precheck.go └── resource │ ├── resoucecheck.go │ └── zz_generated.deepcopy.go ├── cliruntime ├── apply.go ├── delete.go ├── get.go └── patcher.go ├── dag ├── dag.go ├── dag_test.go └── node.go ├── download └── http.go ├── helmsdk ├── helm.go └── repo.go ├── implement ├── helm │ └── helm.go ├── implement.go ├── native │ └── native.go ├── service │ ├── service.go │ └── zz_generated.deepcopy.go └── zz_generated.deepcopy.go ├── plugin └── plugin.go ├── probe ├── exec │ └── exec.go ├── http │ ├── http.go │ └── http_test.go ├── probe.go ├── tcp │ └── tcp.go └── zz_generated.deepcopy.go ├── prober └── prober.go ├── recover └── recover.go └── utils ├── utils.go └── utils_test.go /.gitignore: -------------------------------------------------------------------------------- 1 | 2 | # Binaries for programs and plugins 3 | *.exe 4 | *.exe~ 5 | *.dll 6 | *.so 7 | *.dylib 8 | bin 9 | 10 | # Test binary, build with `go test -c` 11 | *.test 12 | 13 | # Output of the go coverage tool, specifically when used with LiteIDE 14 | *.out 15 | 16 | # Kubernetes Generated files - skip generated files, except for vendored files 17 | 18 | !vendor/**/zz_generated.* 19 | 20 | # editor and IDE paraphernalia 21 | .idea/ 22 | vendor/ 23 | *.swp 24 | *.swo 25 | *~ 26 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | FROM golang:1.14.4-alpine3.11 as builder 2 | ARG VERSION 3 | ARG GIT_REVISION 4 | ARG BUILD_DATE 5 | 6 | RUN apk add --no-cache git 7 | WORKDIR /go/src/ 8 | RUN go get github.com/go-delve/delve/cmd/dlv 9 | 10 | WORKDIR /workspace 11 | ENV GOPATH /workspace 12 | RUN mkdir -p /workspace/src/cloudnativeapp/clm 13 | # Copy the Go Modules manifests 14 | COPY go.mod /workspace/src/cloudnativeapp/clm/go.mod 15 | COPY go.sum /workspace/src/cloudnativeapp/clm/go.sum 16 | # cache deps before building and copying source so that we don't need to re-download as much 17 | # and so that source changes don't invalidate our downloaded layer 18 | RUN cd /workspace/src/cloudnativeapp/clm/ && go mod download 19 | 20 | COPY main.go /workspace/src/cloudnativeapp/clm/main.go 21 | COPY api /workspace/src/cloudnativeapp/clm/api/ 22 | COPY controllers /workspace/src/cloudnativeapp/clm/controllers/ 23 | COPY internal /workspace/src/cloudnativeapp/clm/internal/ 24 | COPY pkg /workspace/src/cloudnativeapp/clm/pkg/ 25 | 26 | ENV CGO_ENABLED=0 GOOS=linux GOARCH=amd64 GO111MODULE=on GIT_REVISION=$GIT_REVISION BUILD_DATE=$BUILD_DATE 27 | 28 | # Build 29 | RUN cd /workspace/src/cloudnativeapp/clm/ && go build -gcflags="all=-N -l" -a -o manager main.go 30 | 31 | FROM alpine:3.10.2 32 | WORKDIR / 33 | COPY --from=builder /workspace/src/cloudnativeapp/clm/manager . 34 | COPY --from=builder /go/bin/dlv / 35 | EXPOSE 40000 36 | RUN apk add tzdata 37 | RUN echo "hosts: files dns" > /etc/nsswitch.conf 38 | RUN cp /usr/share/zoneinfo/Asia/Shanghai /etc/localtime 39 | RUN echo 'Asia/Shanghai' >/etc/timezone 40 | RUN apk add curl 41 | RUN apk del tzdata 42 | ENTRYPOINT ["/manager"] -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | 2 | # Image URL to use all building/pushing image targets 3 | IMG ?= controller:latest 4 | # Produce CRDs that work back to Kubernetes 1.11 (no version conversion) 5 | CRD_OPTIONS ?= "crd:trivialVersions=true" 6 | 7 | # Get the currently used golang install path (in GOPATH/bin, unless GOBIN is set) 8 | ifeq (,$(shell go env GOBIN)) 9 | GOBIN=$(shell go env GOPATH)/bin 10 | else 11 | GOBIN=$(shell go env GOBIN) 12 | endif 13 | 14 | all: manager 15 | 16 | # Run tests 17 | test: generate fmt vet manifests 18 | go test ./... -coverprofile cover.out 19 | 20 | # Build manager binary 21 | manager: generate fmt vet 22 | go build -o bin/manager main.go 23 | 24 | # Run against the configured Kubernetes cluster in ~/.kube/config 25 | run: generate fmt vet manifests 26 | go run ./main.go 27 | 28 | # Install CRDs into a cluster 29 | install: manifests 30 | kustomize build config/crd | kubectl apply -f - 31 | 32 | # Uninstall CRDs from a cluster 33 | uninstall: manifests 34 | kustomize build config/crd | kubectl delete -f - 35 | 36 | # Deploy controller in the configured Kubernetes cluster in ~/.kube/config 37 | deploy: manifests 38 | cd config/manager && kustomize edit set image controller=${IMG} 39 | kustomize build config/default | kubectl apply -f - 40 | 41 | # Generate manifests e.g. CRD, RBAC etc. 42 | manifests: controller-gen 43 | $(CONTROLLER_GEN) $(CRD_OPTIONS) rbac:roleName=manager-role webhook paths="./..." output:crd:artifacts:config=config/crd/bases 44 | 45 | # Run go fmt against code 46 | fmt: 47 | go fmt ./... 48 | 49 | # Run go vet against code 50 | vet: 51 | go vet ./... 52 | 53 | # Generate code 54 | generate: controller-gen 55 | $(CONTROLLER_GEN) object:headerFile="hack/boilerplate.go.txt" paths="./..." 56 | 57 | # Build the docker image 58 | docker-build: test 59 | docker build . -t ${IMG} 60 | 61 | # Push the docker image 62 | docker-push: 63 | docker push ${IMG} 64 | 65 | # find or download controller-gen 66 | # download controller-gen if necessary 67 | controller-gen: 68 | ifeq (, $(shell which controller-gen)) 69 | @{ \ 70 | set -e ;\ 71 | CONTROLLER_GEN_TMP_DIR=$$(mktemp -d) ;\ 72 | cd $$CONTROLLER_GEN_TMP_DIR ;\ 73 | go mod init tmp ;\ 74 | go get sigs.k8s.io/controller-tools/cmd/controller-gen@v0.2.5 ;\ 75 | rm -rf $$CONTROLLER_GEN_TMP_DIR ;\ 76 | } 77 | CONTROLLER_GEN=$(GOBIN)/controller-gen 78 | else 79 | CONTROLLER_GEN=$(shell which controller-gen) 80 | endif 81 | -------------------------------------------------------------------------------- /PROJECT: -------------------------------------------------------------------------------- 1 | domain: cloudnativeapp.io 2 | repo: cloudnativeapp/clm 3 | resources: 4 | - group: clm 5 | kind: CRDRelease 6 | version: v1beta1 7 | - group: clm 8 | kind: Source 9 | version: v1beta1 10 | version: "2" 11 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # CLM 2 | CLM (CRD Lifecycle Management) is a tool to manage CRDs lifecycle in Kubernetes. CLM can contribute to: 3 | 1. Install CRD (yaml file and controller); 4 | 2. Probe status of CRD components; 5 | 3. Recover abnormal CRD components; 6 | 4. Upgrade and downgrade CRD; 7 | 5. Dependency processing of CRD; 8 | 9 | And it can be a special workload of Kubernetes. 10 | 11 | 12 | ## Architecture 13 | CLM contains three components: 14 | ![architecture](images/architecture.jpg) 15 | * Source: Source of CRD release, contains the actually actions to handle CRD release. 16 | * helm source: Using helm-sdk to handle lifecycle actions 17 | * native source: Using cli-runtime 18 | * k8s service source: Using http/https 19 | CLM combine the `helm` and `kubectl` to manage the CRD lifecycle. 20 | * CRDRelease: A release of CRD. 21 | 22 | * Module: The components of a CRDRelease. The smallest entity CLM handle. 23 | 24 | ![module-source](images/module-source.jpg) 25 | 26 | See docs for more information. 27 | 28 | [crdrelease usage](docs/crdrelease.md) 29 | [helm source usage](docs/helm-source.md) 30 | [native source usage](docs/native-source.md) 31 | [service source usage](docs/service-source.md) 32 | 33 | ## Quick Start 34 | * `make install` Install CRD of CLM. 35 | 36 | * `kubectl apply -f clm-server.yaml` Install CLM deployment to K8s cluster. 37 | 38 | * `cd config/sample/; kubectl apply -f source` Install sources of CLM. 39 | 40 | * `kubectl apply -f crdrelease/native-example.yaml` Install the example crd release of native source. 41 | ![crd](images/crd.jpg) 42 | ![nginx](images/nginx-deploy.jpg) 43 | * native.module contains two parts in native source, a remote urls CRD file resource, and a local deployment yaml. 44 | 45 | * `kubectl apply -f crdrelease/helm-example.yaml` Install the example crd release of helm source. 46 | ![helm](images/helm.jpg) 47 | * nginx.module download a nginx helm package and install it. 48 | 49 | * Check the crd releases installed. 50 | ![crdreleases](images/crdreleases.jpg) 51 | 52 | ## Development setup 53 | Please read `Makefile` 54 | 55 | ## Contributing 56 | 1. Fork it 57 | 2. Create your feature branch (git checkout -b feature/fooBar) 58 | 3. Commit your changes (git commit -am 'Add some fooBar') 59 | 4. Push to the branch (git push origin feature/fooBar) 60 | 5. Create a new Pull Request 61 | 62 | ## Meta 63 | Jungle - 353187194@qq.com 64 | Distributed under the Apache 2.0 license. See LICENSE for more information. 65 | 66 | ## Docker Hub 67 | [CLM](https://hub.docker.com/repository/docker/clmgroup/clm) -------------------------------------------------------------------------------- /api/v1beta1/crdrelease_types.go: -------------------------------------------------------------------------------- 1 | /* 2 | 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 v1beta1 18 | 19 | import ( 20 | "cloudnativeapp/clm/internal" 21 | metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" 22 | ) 23 | 24 | // EDIT THIS FILE! THIS IS SCAFFOLDING FOR YOU TO OWN! 25 | // NOTE: json tags are required. Any new fields you add must have json tags for the fields to be serialized. 26 | 27 | // CRDReleaseSpec defines the desired state of CRDRelease 28 | type CRDReleaseSpec struct { 29 | // INSERT ADDITIONAL SPEC FIELDS - desired state of cluster 30 | // Important: Run "make" to regenerate code after modifying this file 31 | 32 | // The version of CRDRelease to be intalled, Unique combine with release name 33 | Version string `json:"version,omitempty"` 34 | // Dependencies to install this CRDRelease 35 | Dependencies []internal.Dependency `json:"dependencies,omitempty"` 36 | // CRDRelease consist of multi modules, every module implements part of functions of release 37 | Modules []internal.Module `json:"modules,omitempty"` 38 | } 39 | 40 | // CRDReleaseStatus defines the observed state of CRDRelease 41 | type CRDReleaseStatus struct { 42 | // INSERT ADDITIONAL STATUS FIELD - define observed state of cluster 43 | // Important: Run "make" to regenerate code after modifying this file 44 | Conditions []internal.CRDReleaseCondition `json:"conditions,omitempty"` 45 | Dependencies []internal.DependencyStatus `json:"dependencies,omitempty"` 46 | Modules []internal.ModuleStatus `json:"modules,omitempty"` 47 | // CurrentVersion changed after install release successully. 48 | CurrentVersion string `json:"currentVersion,omitempty"` 49 | Phase internal.CRDReleasePhase `json:"phase,omitempty"` 50 | Reason string `json:"reason,omitempty"` 51 | } 52 | 53 | // +kubebuilder:object:root=true 54 | // +kubebuilder:resource:scope=Cluster 55 | 56 | // CRDRelease is the Schema for the crdreleases API 57 | type CRDRelease struct { 58 | metav1.TypeMeta `json:",inline"` 59 | metav1.ObjectMeta `json:"metadata,omitempty"` 60 | 61 | Spec CRDReleaseSpec `json:"spec,omitempty"` 62 | Status CRDReleaseStatus `json:"status,omitempty"` 63 | } 64 | 65 | // +kubebuilder:object:root=true 66 | 67 | // CRDReleaseList contains a list of CRDRelease 68 | type CRDReleaseList struct { 69 | metav1.TypeMeta `json:",inline"` 70 | metav1.ListMeta `json:"metadata,omitempty"` 71 | Items []CRDRelease `json:"items"` 72 | } 73 | 74 | func init() { 75 | SchemeBuilder.Register(&CRDRelease{}, &CRDReleaseList{}) 76 | } 77 | -------------------------------------------------------------------------------- /api/v1beta1/groupversion_info.go: -------------------------------------------------------------------------------- 1 | /* 2 | 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 v1beta1 contains API Schema definitions for the clm v1beta1 API group 18 | // +kubebuilder:object:generate=true 19 | // +groupName=clm.cloudnativeapp.io 20 | package v1beta1 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: "clm.cloudnativeapp.io", Version: "v1beta1"} 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 | -------------------------------------------------------------------------------- /api/v1beta1/source_types.go: -------------------------------------------------------------------------------- 1 | /* 2 | 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 v1beta1 18 | 19 | import ( 20 | "cloudnativeapp/clm/pkg/implement" 21 | metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" 22 | ) 23 | 24 | // EDIT THIS FILE! THIS IS SCAFFOLDING FOR YOU TO OWN! 25 | // NOTE: json tags are required. Any new fields you add must have json tags for the fields to be serialized. 26 | 27 | // SourceSpec defines the desired state of Source 28 | type SourceSpec struct { 29 | // INSERT ADDITIONAL SPEC FIELDS - desired state of cluster 30 | // Important: Run "make" to regenerate code after modifying this file 31 | 32 | // Foo is an example field of Source. Edit Source_types.go to remove/update 33 | Type string `json:"type,omitempty"` 34 | Implement implement.Implement `json:"implement,omitempty"` 35 | } 36 | 37 | // SourceStatus defines the observed state of Source 38 | type SourceStatus struct { 39 | // INSERT ADDITIONAL STATUS FIELD - define observed state of cluster 40 | // Important: Run "make" to regenerate code after modifying this file 41 | Ready bool `json:"ready,omitempty"` 42 | } 43 | 44 | // +kubebuilder:object:root=true 45 | // +kubebuilder:resource:scope=Cluster 46 | 47 | // Source is the Schema for the sources API 48 | type Source struct { 49 | metav1.TypeMeta `json:",inline"` 50 | metav1.ObjectMeta `json:"metadata,omitempty"` 51 | 52 | Spec SourceSpec `json:"spec,omitempty"` 53 | Status SourceStatus `json:"status,omitempty"` 54 | } 55 | 56 | // +kubebuilder:object:root=true 57 | 58 | // SourceList contains a list of Source 59 | type SourceList struct { 60 | metav1.TypeMeta `json:",inline"` 61 | metav1.ListMeta `json:"metadata,omitempty"` 62 | Items []Source `json:"items"` 63 | } 64 | 65 | func init() { 66 | SchemeBuilder.Register(&Source{}, &SourceList{}) 67 | } 68 | -------------------------------------------------------------------------------- /api/v1beta1/zz_generated.deepcopy.go: -------------------------------------------------------------------------------- 1 | // +build !ignore_autogenerated 2 | 3 | /* 4 | 5 | 6 | Licensed under the Apache License, Version 2.0 (the "License"); 7 | you may not use this file except in compliance with the License. 8 | You may obtain a copy of the License at 9 | 10 | http://www.apache.org/licenses/LICENSE-2.0 11 | 12 | Unless required by applicable law or agreed to in writing, software 13 | distributed under the License is distributed on an "AS IS" BASIS, 14 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 15 | See the License for the specific language governing permissions and 16 | limitations under the License. 17 | */ 18 | 19 | // Code generated by controller-gen. DO NOT EDIT. 20 | 21 | package v1beta1 22 | 23 | import ( 24 | "cloudnativeapp/clm/internal" 25 | runtime "k8s.io/apimachinery/pkg/runtime" 26 | ) 27 | 28 | // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. 29 | func (in *CRDRelease) DeepCopyInto(out *CRDRelease) { 30 | *out = *in 31 | out.TypeMeta = in.TypeMeta 32 | in.ObjectMeta.DeepCopyInto(&out.ObjectMeta) 33 | in.Spec.DeepCopyInto(&out.Spec) 34 | in.Status.DeepCopyInto(&out.Status) 35 | } 36 | 37 | // DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new CRDRelease. 38 | func (in *CRDRelease) DeepCopy() *CRDRelease { 39 | if in == nil { 40 | return nil 41 | } 42 | out := new(CRDRelease) 43 | in.DeepCopyInto(out) 44 | return out 45 | } 46 | 47 | // DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object. 48 | func (in *CRDRelease) DeepCopyObject() runtime.Object { 49 | if c := in.DeepCopy(); c != nil { 50 | return c 51 | } 52 | return nil 53 | } 54 | 55 | // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. 56 | func (in *CRDReleaseList) DeepCopyInto(out *CRDReleaseList) { 57 | *out = *in 58 | out.TypeMeta = in.TypeMeta 59 | in.ListMeta.DeepCopyInto(&out.ListMeta) 60 | if in.Items != nil { 61 | in, out := &in.Items, &out.Items 62 | *out = make([]CRDRelease, len(*in)) 63 | for i := range *in { 64 | (*in)[i].DeepCopyInto(&(*out)[i]) 65 | } 66 | } 67 | } 68 | 69 | // DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new CRDReleaseList. 70 | func (in *CRDReleaseList) DeepCopy() *CRDReleaseList { 71 | if in == nil { 72 | return nil 73 | } 74 | out := new(CRDReleaseList) 75 | in.DeepCopyInto(out) 76 | return out 77 | } 78 | 79 | // DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object. 80 | func (in *CRDReleaseList) DeepCopyObject() runtime.Object { 81 | if c := in.DeepCopy(); c != nil { 82 | return c 83 | } 84 | return nil 85 | } 86 | 87 | // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. 88 | func (in *CRDReleaseSpec) DeepCopyInto(out *CRDReleaseSpec) { 89 | *out = *in 90 | if in.Dependencies != nil { 91 | in, out := &in.Dependencies, &out.Dependencies 92 | *out = make([]internal.Dependency, len(*in)) 93 | for i := range *in { 94 | (*in)[i].DeepCopyInto(&(*out)[i]) 95 | } 96 | } 97 | if in.Modules != nil { 98 | in, out := &in.Modules, &out.Modules 99 | *out = make([]internal.Module, len(*in)) 100 | for i := range *in { 101 | (*in)[i].DeepCopyInto(&(*out)[i]) 102 | } 103 | } 104 | } 105 | 106 | // DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new CRDReleaseSpec. 107 | func (in *CRDReleaseSpec) DeepCopy() *CRDReleaseSpec { 108 | if in == nil { 109 | return nil 110 | } 111 | out := new(CRDReleaseSpec) 112 | in.DeepCopyInto(out) 113 | return out 114 | } 115 | 116 | // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. 117 | func (in *CRDReleaseStatus) DeepCopyInto(out *CRDReleaseStatus) { 118 | *out = *in 119 | if in.Conditions != nil { 120 | in, out := &in.Conditions, &out.Conditions 121 | *out = make([]internal.CRDReleaseCondition, len(*in)) 122 | for i := range *in { 123 | (*in)[i].DeepCopyInto(&(*out)[i]) 124 | } 125 | } 126 | if in.Dependencies != nil { 127 | in, out := &in.Dependencies, &out.Dependencies 128 | *out = make([]internal.DependencyStatus, len(*in)) 129 | copy(*out, *in) 130 | } 131 | if in.Modules != nil { 132 | in, out := &in.Modules, &out.Modules 133 | *out = make([]internal.ModuleStatus, len(*in)) 134 | for i := range *in { 135 | (*in)[i].DeepCopyInto(&(*out)[i]) 136 | } 137 | } 138 | } 139 | 140 | // DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new CRDReleaseStatus. 141 | func (in *CRDReleaseStatus) DeepCopy() *CRDReleaseStatus { 142 | if in == nil { 143 | return nil 144 | } 145 | out := new(CRDReleaseStatus) 146 | in.DeepCopyInto(out) 147 | return out 148 | } 149 | 150 | // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. 151 | func (in *Source) DeepCopyInto(out *Source) { 152 | *out = *in 153 | out.TypeMeta = in.TypeMeta 154 | in.ObjectMeta.DeepCopyInto(&out.ObjectMeta) 155 | in.Spec.DeepCopyInto(&out.Spec) 156 | out.Status = in.Status 157 | } 158 | 159 | // DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new Source. 160 | func (in *Source) DeepCopy() *Source { 161 | if in == nil { 162 | return nil 163 | } 164 | out := new(Source) 165 | in.DeepCopyInto(out) 166 | return out 167 | } 168 | 169 | // DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object. 170 | func (in *Source) DeepCopyObject() runtime.Object { 171 | if c := in.DeepCopy(); c != nil { 172 | return c 173 | } 174 | return nil 175 | } 176 | 177 | // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. 178 | func (in *SourceList) DeepCopyInto(out *SourceList) { 179 | *out = *in 180 | out.TypeMeta = in.TypeMeta 181 | in.ListMeta.DeepCopyInto(&out.ListMeta) 182 | if in.Items != nil { 183 | in, out := &in.Items, &out.Items 184 | *out = make([]Source, len(*in)) 185 | for i := range *in { 186 | (*in)[i].DeepCopyInto(&(*out)[i]) 187 | } 188 | } 189 | } 190 | 191 | // DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new SourceList. 192 | func (in *SourceList) DeepCopy() *SourceList { 193 | if in == nil { 194 | return nil 195 | } 196 | out := new(SourceList) 197 | in.DeepCopyInto(out) 198 | return out 199 | } 200 | 201 | // DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object. 202 | func (in *SourceList) DeepCopyObject() runtime.Object { 203 | if c := in.DeepCopy(); c != nil { 204 | return c 205 | } 206 | return nil 207 | } 208 | 209 | // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. 210 | func (in *SourceSpec) DeepCopyInto(out *SourceSpec) { 211 | *out = *in 212 | in.Implement.DeepCopyInto(&out.Implement) 213 | } 214 | 215 | // DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new SourceSpec. 216 | func (in *SourceSpec) DeepCopy() *SourceSpec { 217 | if in == nil { 218 | return nil 219 | } 220 | out := new(SourceSpec) 221 | in.DeepCopyInto(out) 222 | return out 223 | } 224 | 225 | // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. 226 | func (in *SourceStatus) DeepCopyInto(out *SourceStatus) { 227 | *out = *in 228 | } 229 | 230 | // DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new SourceStatus. 231 | func (in *SourceStatus) DeepCopy() *SourceStatus { 232 | if in == nil { 233 | return nil 234 | } 235 | out := new(SourceStatus) 236 | in.DeepCopyInto(out) 237 | return out 238 | } 239 | -------------------------------------------------------------------------------- /clm-server.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: v1 2 | kind: Namespace 3 | metadata: 4 | name: clm-system 5 | --- 6 | apiVersion: rbac.authorization.k8s.io/v1 7 | kind: ClusterRole 8 | metadata: 9 | name: clm-role 10 | rules: 11 | - apiGroups: ["*"] 12 | resources: ["*"] 13 | verbs: ["*"] 14 | --- 15 | apiVersion: rbac.authorization.k8s.io/v1 16 | kind: ClusterRoleBinding 17 | metadata: 18 | name: clm-role-binding 19 | subjects: 20 | - kind: ServiceAccount 21 | name: clm-service-account 22 | namespace: clm-system 23 | roleRef: 24 | kind: ClusterRole 25 | name: clm-role 26 | apiGroup: rbac.authorization.k8s.io 27 | --- 28 | apiVersion: v1 29 | kind: ServiceAccount 30 | metadata: 31 | name: clm-service-account 32 | namespace: clm-system 33 | --- 34 | apiVersion: apps/v1 35 | kind: Deployment 36 | metadata: 37 | name: clm-server 38 | namespace: clm-system 39 | labels: 40 | app: clm-server 41 | src: cloudnativeapp 42 | spec: 43 | replicas: 1 44 | selector: 45 | matchLabels: 46 | app: clm-server 47 | src: cloudnativeapp 48 | template: 49 | metadata: 50 | labels: 51 | app: clm-server 52 | src: cloudnativeapp 53 | spec: 54 | serviceAccountName: clm-service-account 55 | containers: 56 | - name: clm-server 57 | image: clmgroup/clm:release-2.0.0 58 | imagePullPolicy: Always 59 | args: 60 | - --enable-log-file=true 61 | - --log-level=debug 62 | securityContext: 63 | privileged: true 64 | restartPolicy: Always -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /config/certmanager/kustomization.yaml: -------------------------------------------------------------------------------- 1 | resources: 2 | - certificate.yaml 3 | 4 | configurations: 5 | - kustomizeconfig.yaml 6 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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/clm.cloudnativeapp.io_crdreleases.yaml 6 | - bases/clm.cloudnativeapp.io_sources.yaml 7 | # +kubebuilder:scaffold:crdkustomizeresource 8 | 9 | patchesStrategicMerge: 10 | # [WEBHOOK] To enable webhook, uncomment all the sections with [WEBHOOK] prefix. 11 | # patches here are for enabling the conversion webhook for each CRD 12 | #- patches/webhook_in_crdreleases.yaml 13 | #- patches/webhook_in_sources.yaml 14 | # +kubebuilder:scaffold:crdkustomizewebhookpatch 15 | 16 | # [CERTMANAGER] To enable webhook, uncomment all the sections with [CERTMANAGER] prefix. 17 | # patches here are for enabling the CA injection for each CRD 18 | #- patches/cainjection_in_crdreleases.yaml 19 | #- patches/cainjection_in_sources.yaml 20 | # +kubebuilder:scaffold:crdkustomizecainjectionpatch 21 | 22 | # the following config is for teaching kustomize how to do kustomization for CRDs. 23 | configurations: 24 | - kustomizeconfig.yaml 25 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /config/crd/patches/cainjection_in_crdreleases.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: crdreleases.clm.cloudnativeapp.io 9 | -------------------------------------------------------------------------------- /config/crd/patches/cainjection_in_sources.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: sources.clm.cloudnativeapp.io 9 | -------------------------------------------------------------------------------- /config/crd/patches/webhook_in_crdreleases.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: crdreleases.clm.cloudnativeapp.io 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_sources.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: sources.clm.cloudnativeapp.io 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/default/kustomization.yaml: -------------------------------------------------------------------------------- 1 | # Adds namespace to all resources. 2 | namespace: clm-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: clm- 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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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/manager/kustomization.yaml: -------------------------------------------------------------------------------- 1 | resources: 2 | - manager.yaml 3 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /config/prometheus/kustomization.yaml: -------------------------------------------------------------------------------- 1 | resources: 2 | - monitor.yaml 3 | -------------------------------------------------------------------------------- /config/prometheus/monitor.yaml: -------------------------------------------------------------------------------- 1 | 2 | # Prometheus Monitor Service (Metrics) 3 | apiVersion: monitoring.coreos.com/v1 4 | kind: ServiceMonitor 5 | metadata: 6 | labels: 7 | control-plane: controller-manager 8 | name: controller-manager-metrics-monitor 9 | namespace: system 10 | spec: 11 | endpoints: 12 | - path: /metrics 13 | port: https 14 | selector: 15 | matchLabels: 16 | control-plane: controller-manager 17 | -------------------------------------------------------------------------------- /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/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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /config/rbac/auth_proxy_service.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: v1 2 | kind: Service 3 | metadata: 4 | labels: 5 | control-plane: controller-manager 6 | name: controller-manager-metrics-service 7 | namespace: system 8 | spec: 9 | ports: 10 | - name: https 11 | port: 8443 12 | targetPort: https 13 | selector: 14 | control-plane: controller-manager 15 | -------------------------------------------------------------------------------- /config/rbac/crdrelease_editor_role.yaml: -------------------------------------------------------------------------------- 1 | # permissions for end users to edit crdreleases. 2 | apiVersion: rbac.authorization.k8s.io/v1 3 | kind: ClusterRole 4 | metadata: 5 | name: crdrelease-editor-role 6 | rules: 7 | - apiGroups: 8 | - clm.cloudnativeapp.io 9 | resources: 10 | - crdreleases 11 | verbs: 12 | - create 13 | - delete 14 | - get 15 | - list 16 | - patch 17 | - update 18 | - watch 19 | - apiGroups: 20 | - clm.cloudnativeapp.io 21 | resources: 22 | - crdreleases/status 23 | verbs: 24 | - get 25 | -------------------------------------------------------------------------------- /config/rbac/crdrelease_viewer_role.yaml: -------------------------------------------------------------------------------- 1 | # permissions for end users to view crdreleases. 2 | apiVersion: rbac.authorization.k8s.io/v1 3 | kind: ClusterRole 4 | metadata: 5 | name: crdrelease-viewer-role 6 | rules: 7 | - apiGroups: 8 | - clm.cloudnativeapp.io 9 | resources: 10 | - crdreleases 11 | verbs: 12 | - get 13 | - list 14 | - watch 15 | - apiGroups: 16 | - clm.cloudnativeapp.io 17 | resources: 18 | - crdreleases/status 19 | verbs: 20 | - get 21 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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/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 | - clm.cloudnativeapp.io 11 | resources: 12 | - crdreleases 13 | verbs: 14 | - create 15 | - delete 16 | - get 17 | - list 18 | - patch 19 | - update 20 | - watch 21 | - apiGroups: 22 | - clm.cloudnativeapp.io 23 | resources: 24 | - crdreleases/status 25 | verbs: 26 | - get 27 | - patch 28 | - update 29 | - apiGroups: 30 | - clm.cloudnativeapp.io 31 | resources: 32 | - events 33 | verbs: 34 | - create 35 | - get 36 | - list 37 | - patch 38 | - update 39 | - watch 40 | - apiGroups: 41 | - clm.cloudnativeapp.io 42 | resources: 43 | - sources 44 | verbs: 45 | - create 46 | - delete 47 | - get 48 | - list 49 | - patch 50 | - update 51 | - watch 52 | - apiGroups: 53 | - clm.cloudnativeapp.io 54 | resources: 55 | - sources/status 56 | verbs: 57 | - get 58 | - patch 59 | - update 60 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /config/rbac/source_editor_role.yaml: -------------------------------------------------------------------------------- 1 | # permissions for end users to edit sources. 2 | apiVersion: rbac.authorization.k8s.io/v1 3 | kind: ClusterRole 4 | metadata: 5 | name: source-editor-role 6 | rules: 7 | - apiGroups: 8 | - clm.cloudnativeapp.io 9 | resources: 10 | - sources 11 | verbs: 12 | - create 13 | - delete 14 | - get 15 | - list 16 | - patch 17 | - update 18 | - watch 19 | - apiGroups: 20 | - clm.cloudnativeapp.io 21 | resources: 22 | - sources/status 23 | verbs: 24 | - get 25 | -------------------------------------------------------------------------------- /config/rbac/source_viewer_role.yaml: -------------------------------------------------------------------------------- 1 | # permissions for end users to view sources. 2 | apiVersion: rbac.authorization.k8s.io/v1 3 | kind: ClusterRole 4 | metadata: 5 | name: source-viewer-role 6 | rules: 7 | - apiGroups: 8 | - clm.cloudnativeapp.io 9 | resources: 10 | - sources 11 | verbs: 12 | - get 13 | - list 14 | - watch 15 | - apiGroups: 16 | - clm.cloudnativeapp.io 17 | resources: 18 | - sources/status 19 | verbs: 20 | - get 21 | -------------------------------------------------------------------------------- /config/sample/crdrelease/helm-example.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: clm.cloudnativeapp.io/v1beta1 2 | kind: CRDRelease 3 | metadata: 4 | name: test-helm 5 | spec: 6 | version: 1.0.0 7 | modules: 8 | - name: nginx.module 9 | source: 10 | name: helm-source 11 | values: 12 | chartPath: "https://cloudnativeapp.oss-cn-shenzhen.aliyuncs.com/clm/nginx-ingress-0.7.1.tgz" 13 | namespace: default 14 | releaseName: nginx -------------------------------------------------------------------------------- /config/sample/crdrelease/helm-repo-example.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: clm.cloudnativeapp.io/v1beta1 2 | kind: CRDRelease 3 | metadata: 4 | name: test-helm-repo 5 | spec: 6 | version: 1.0.0 7 | modules: 8 | - name: nginx.module 9 | source: 10 | name: helm-source 11 | values: 12 | chartPath: "bitnami/nginx" 13 | namespace: default 14 | releaseName: bitnginx -------------------------------------------------------------------------------- /config/sample/crdrelease/native-example.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: clm.cloudnativeapp.io/v1beta1 2 | kind: CRDRelease 3 | metadata: 4 | name: test-native 5 | spec: 6 | version: 1.0.0 7 | modules: 8 | - name: native.module 9 | conditions: 10 | resourceNotExist: 11 | - type: CustomResourceDefinition 12 | name: scaledobjects.keda.k8s.io 13 | source: 14 | name: native-source 15 | values: 16 | urls: 17 | - https://cloudnativeapp.oss-cn-shenzhen.aliyuncs.com/clm/scaledobjects-crd.yaml 18 | yaml: | 19 | apiVersion: apps/v1 20 | kind: Deployment 21 | metadata: 22 | name: nginx-deployment 23 | namespace: default 24 | spec: 25 | selector: 26 | matchLabels: 27 | app: nginx 28 | replicas: 2 29 | template: 30 | metadata: 31 | labels: 32 | app: nginx 33 | spec: 34 | containers: 35 | - name: nginx 36 | image: nginx:1.14.2 37 | ports: 38 | - containerPort: 80 -------------------------------------------------------------------------------- /config/sample/import/helm-import-example.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: clm.cloudnativeapp.io/v1beta1 2 | kind: CRDRelease 3 | metadata: 4 | name: test-helm-import 5 | spec: 6 | version: 1.0.0 7 | modules: 8 | - name: native.module 9 | conditions: 10 | resourceNotExist: 11 | - type: Deployment 12 | name: bitnginx 13 | namespace: default 14 | strategy: Import # Take it as imported when conditions failed 15 | source: 16 | name: helm-source 17 | values: 18 | chartPath: "bitnami/nginx" 19 | namespace: default 20 | releaseName: bitnginx -------------------------------------------------------------------------------- /config/sample/import/native-import-example.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: clm.cloudnativeapp.io/v1beta1 2 | kind: CRDRelease 3 | metadata: 4 | name: test-native-import 5 | spec: 6 | version: 1.0.0 7 | modules: 8 | - name: native.module 9 | conditions: 10 | resourceNotExist: 11 | - type: Deployment 12 | name: nginx-deployment 13 | namespace: default 14 | strategy: Import # Take it as imported when conditions failed 15 | source: 16 | name: native-source 17 | values: 18 | yaml: | 19 | apiVersion: apps/v1 20 | kind: Deployment 21 | metadata: 22 | name: nginx-deployment 23 | namespace: default 24 | spec: 25 | selector: 26 | matchLabels: 27 | app: nginx 28 | replicas: 2 29 | template: 30 | metadata: 31 | labels: 32 | app: nginx 33 | spec: 34 | containers: 35 | - name: nginx 36 | image: nginx:1.14.1 37 | ports: 38 | - containerPort: 80 -------------------------------------------------------------------------------- /config/sample/import/nginx-deployment.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: apps/v1 2 | kind: Deployment 3 | metadata: 4 | name: nginx-deployment 5 | namespace: default 6 | spec: 7 | selector: 8 | matchLabels: 9 | app: nginx 10 | replicas: 2 11 | template: 12 | metadata: 13 | labels: 14 | app: nginx 15 | spec: 16 | containers: 17 | - name: nginx 18 | image: nginx:1.14.1 19 | ports: 20 | - containerPort: 80 -------------------------------------------------------------------------------- /config/sample/source/helm-source.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: clm.cloudnativeapp.io/v1beta1 2 | kind: Source 3 | metadata: 4 | name: helm-source 5 | spec: 6 | type: helm 7 | implement: 8 | helm: 9 | wait: true 10 | timeout: 120 11 | repositories: 12 | - name: bitnami 13 | url: https://charts.bitnami.com/bitnami 14 | - name: nginx-stable 15 | url: https://helm.nginx.com/stable -------------------------------------------------------------------------------- /config/sample/source/native-source.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: clm.cloudnativeapp.io/v1beta1 2 | kind: Source 3 | metadata: 4 | name: native-source 5 | spec: 6 | type: native 7 | implement: 8 | native: 9 | ignoreError: false -------------------------------------------------------------------------------- /config/webhook/kustomization.yaml: -------------------------------------------------------------------------------- 1 | resources: 2 | - manifests.yaml 3 | - service.yaml 4 | 5 | configurations: 6 | - kustomizeconfig.yaml 7 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /controllers/crdrelease_controller.go: -------------------------------------------------------------------------------- 1 | /* 2 | 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 | "cloudnativeapp/clm/internal" 21 | "cloudnativeapp/clm/pkg/utils" 22 | "context" 23 | "errors" 24 | v1 "k8s.io/api/core/v1" 25 | "time" 26 | 27 | "github.com/go-logr/logr" 28 | "k8s.io/apimachinery/pkg/runtime" 29 | "k8s.io/client-go/tools/record" 30 | ctrl "sigs.k8s.io/controller-runtime" 31 | "sigs.k8s.io/controller-runtime/pkg/client" 32 | "sigs.k8s.io/controller-runtime/pkg/reconcile" 33 | 34 | clmv1beta1 "cloudnativeapp/clm/api/v1beta1" 35 | ) 36 | 37 | // CRDReleaseReconciler reconciles a CRDRelease object 38 | type CRDReleaseReconciler struct { 39 | client.Client 40 | Log logr.Logger 41 | Scheme *runtime.Scheme 42 | Eventer record.EventRecorder 43 | } 44 | 45 | const ReleaseFinalizer = "finalizer.clm.cloudnativeapp.io" 46 | const CycleDelay = 10 47 | 48 | // +kubebuilder:rbac:groups=clm.cloudnativeapp.io,resources=crdreleases,verbs=get;list;watch;create;update;patch;delete 49 | // +kubebuilder:rbac:groups=clm.cloudnativeapp.io,resources=crdreleases/status,verbs=get;update;patch 50 | // +kubebuilder:rbac:groups=clm.cloudnativeapp.io,resources=events,verbs=get;list;watch;create;update;patch 51 | 52 | func (r *CRDReleaseReconciler) Reconcile(req ctrl.Request) (ctrl.Result, error) { 53 | ctx := context.Background() 54 | log := r.Log.WithValues("namespace", req.Namespace, 55 | "crd release", req.Name) 56 | 57 | // your logic here 58 | release := &clmv1beta1.CRDRelease{} 59 | if err := r.Get(ctx, req.NamespacedName, release); err != nil { 60 | return ctrl.Result{}, client.IgnoreNotFound(err) 61 | } 62 | log.V(utils.Info).Info("succeed get release", "name", release.Name, "version", release.Spec.Version) 63 | log.V(utils.Debug).Info("source values", "value", release.Spec.Modules[0].Source.Values) 64 | // Record crd release status. 65 | if !internal.RecordStatus(release.Name, release.Spec.Version, *release.Status.DeepCopy()) { 66 | // retry later 67 | releaseLog.V(utils.Info).Info("crd release is processing, retry later.") 68 | return ctrl.Result{Requeue: true, RequeueAfter: 10 * time.Second}, nil 69 | } 70 | defer func() { 71 | internal.DeleteStatus(release.Name, release.Spec.Version) 72 | if p := recover(); p != nil { 73 | log.Error(errors.New("panic occurs"), "panic occurs", "panic:", p) 74 | r.updateRelease(log, internal.CRDReleaseAbnormal, "panic", release) 75 | r.Eventer.Eventf(release, v1.EventTypeWarning, "Panic", "panic:%v", p) 76 | } 77 | }() 78 | 79 | if release.GetDeletionTimestamp() != nil { 80 | log.V(utils.Info).Info("crd release is going to be deleted", "name", release.Name, 81 | "version", release.Spec.Version) 82 | r.Eventer.Eventf(release, v1.EventTypeNormal, "Deleting", "try to finalize") 83 | if utils.Contains(release.GetFinalizers(), ReleaseFinalizer) { 84 | ok, err := r.finalizeRelease(log, release) 85 | if err != nil { 86 | r.Eventer.Eventf(release, v1.EventTypeWarning, "Error", "finalize error:%v", err) 87 | return reconcile.Result{}, err 88 | } 89 | if !ok { 90 | log.V(utils.Warn).Info("finalize crdRelease retry later") 91 | return ctrl.Result{Requeue: true, RequeueAfter: 1 * time.Second}, nil 92 | } 93 | } 94 | return reconcile.Result{}, nil 95 | } 96 | 97 | if !utils.Contains(release.GetFinalizers(), ReleaseFinalizer) { 98 | if err := r.addFinalizer(log, release); err != nil { 99 | return reconcile.Result{}, err 100 | } 101 | } 102 | 103 | var reason string 104 | var crdReleasePhase internal.CRDReleasePhase 105 | if ok, err := CheckCRDRelease(release); err != nil { 106 | log.Error(err, "crd release check error", "spec", release.Spec, "status", release.Status) 107 | r.Eventer.Eventf(release, v1.EventTypeWarning, "Error", "crd release check error:%v", err) 108 | reason = err.Error() 109 | // not all error should turn to crd release abnormal 110 | if abnormalCheck(*release, err) { 111 | crdReleasePhase = internal.CRDReleaseAbnormal 112 | } else { 113 | crdReleasePhase = internal.CRDReleaseInstalling 114 | } 115 | } else if !ok { 116 | log.V(utils.Info).Info("crd release not ready", "name", release.Name, 117 | "version", release.Spec.Version) 118 | crdReleasePhase = internal.CRDReleaseInstalling 119 | } else { 120 | log.V(utils.Info).Info("crd release is running", "name", release.Name, 121 | "version", release.Spec.Version) 122 | crdReleasePhase = internal.CRDReleaseRunning 123 | } 124 | 125 | if updated, err := r.updateRelease(log, crdReleasePhase, reason, release); err != nil { 126 | log.Error(err, "updateRelease error") 127 | r.Eventer.Eventf(release, v1.EventTypeWarning, "Error", "updateRelease error:%v", err) 128 | // 出现无法控制的故障,requeue也毫无意义 129 | return ctrl.Result{}, err 130 | } else if updated { 131 | return ctrl.Result{}, nil 132 | } else { 133 | return ctrl.Result{Requeue: true, RequeueAfter: 10 * time.Second}, nil 134 | } 135 | } 136 | 137 | // Think twice before turn crd release phase to abnormal 138 | func abnormalCheck(release clmv1beta1.CRDRelease, err error) bool { 139 | if err.Error() == utils.ModuleStateAbnormal { 140 | for _, m := range release.Status.Modules { 141 | if m.State.Abnormal != nil && m.State.Abnormal.Reason != utils.ImplementNotFound { 142 | return true 143 | } 144 | } 145 | } 146 | if err.Error() == utils.DependencyStateAbnormal { 147 | for _, d := range release.Status.Dependencies { 148 | if d.Phase == internal.DependencyAbnormal && d.Reason != utils.ImplementNotFound { 149 | return true 150 | } 151 | } 152 | } 153 | return false 154 | } 155 | 156 | func (r *CRDReleaseReconciler) SetupWithManager(mgr ctrl.Manager) error { 157 | return ctrl.NewControllerManagedBy(mgr). 158 | For(&clmv1beta1.CRDRelease{}). 159 | Complete(r) 160 | } 161 | 162 | func (r *CRDReleaseReconciler) finalizeRelease(reqLogger logr.Logger, release *clmv1beta1.CRDRelease) (bool, error) { 163 | reqLogger.V(utils.Debug).Info("crdRelease finalizer", "name", release.Name) 164 | ok, err := UninstallCRDRelease(release) 165 | if err != nil { 166 | reqLogger.Error(err, "uninstall crd release failed", "name", release.Name) 167 | r.Update(context.TODO(), release) 168 | return false, err 169 | } 170 | if !ok { 171 | return false, nil 172 | } 173 | release.SetFinalizers(utils.Remove(release.GetFinalizers(), ReleaseFinalizer)) 174 | if err := r.Update(context.TODO(), release); err != nil { 175 | reqLogger.Error(err, "failed to update crd release in finalizer", "name", release.Name) 176 | return false, err 177 | } 178 | return true, nil 179 | } 180 | 181 | func (r *CRDReleaseReconciler) addFinalizer(reqLogger logr.Logger, release *clmv1beta1.CRDRelease) error { 182 | finalizers := release.GetFinalizers() 183 | finalizers = append(finalizers, ReleaseFinalizer) 184 | reqLogger.V(utils.Debug).Info("Adding Finalizer for the feature", "feature", release.Name) 185 | 186 | release.SetFinalizers(finalizers) 187 | return nil 188 | } 189 | 190 | func (r *CRDReleaseReconciler) updateRelease(reqLogger logr.Logger, phase internal.CRDReleasePhase, 191 | reason string, release *clmv1beta1.CRDRelease) (bool, error) { 192 | updateCRDReleaseStatus(release, phase, reason) 193 | if changed, err := updateReleaseCheck(release); err != nil { 194 | return false, err 195 | } else if changed { 196 | if err := r.Update(context.Background(), release); err != nil { 197 | reqLogger.Error(err, "update crdRelease failed", "name", release.Name, "version", release.Spec.Version, 198 | "phase", phase, "reason", reason) 199 | return true, err 200 | } 201 | return true, nil 202 | } 203 | return false, nil 204 | } 205 | -------------------------------------------------------------------------------- /controllers/source_controller.go: -------------------------------------------------------------------------------- 1 | /* 2 | 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 | "cloudnativeapp/clm/internal" 21 | "cloudnativeapp/clm/pkg/helmsdk" 22 | "cloudnativeapp/clm/pkg/utils" 23 | "context" 24 | v1 "k8s.io/api/core/v1" 25 | "k8s.io/client-go/tools/record" 26 | "sigs.k8s.io/controller-runtime/pkg/reconcile" 27 | 28 | "github.com/go-logr/logr" 29 | "k8s.io/apimachinery/pkg/runtime" 30 | ctrl "sigs.k8s.io/controller-runtime" 31 | "sigs.k8s.io/controller-runtime/pkg/client" 32 | 33 | clmv1beta1 "cloudnativeapp/clm/api/v1beta1" 34 | ) 35 | 36 | // SourceReconciler reconciles a Source object 37 | type SourceReconciler struct { 38 | client.Client 39 | Log logr.Logger 40 | Scheme *runtime.Scheme 41 | Eventer record.EventRecorder 42 | } 43 | 44 | const sourceFinalizer = "finalizer.clm.cloudnativeapp.io" 45 | 46 | // +kubebuilder:rbac:groups=clm.cloudnativeapp.io,resources=sources,verbs=get;list;watch;create;update;patch;delete 47 | // +kubebuilder:rbac:groups=clm.cloudnativeapp.io,resources=sources/status,verbs=get;update;patch 48 | 49 | func (r *SourceReconciler) Reconcile(req ctrl.Request) (ctrl.Result, error) { 50 | ctx := context.Background() 51 | log := r.Log.WithValues("source", req.NamespacedName) 52 | 53 | // your logic here 54 | source := &clmv1beta1.Source{} 55 | if err := r.Get(ctx, req.NamespacedName, source); err != nil { 56 | return ctrl.Result{}, client.IgnoreNotFound(err) 57 | } 58 | log.V(utils.Info).Info("succeed get source", "name", source.Name, "type", source.Spec.Type) 59 | if source.GetDeletionTimestamp() != nil { 60 | log.V(utils.Debug).Info("try to finalize source", "name", source.Name) 61 | r.Eventer.Eventf(source, v1.EventTypeNormal, "Deleting", "try to finalize source") 62 | if utils.Contains(source.GetFinalizers(), sourceFinalizer) { 63 | err := r.finalizeSource(log, source) 64 | if err != nil { 65 | return reconcile.Result{}, err 66 | } 67 | } 68 | } 69 | 70 | if !utils.Contains(source.GetFinalizers(), sourceFinalizer) { 71 | if err := r.addFinalizer(log, source); err != nil { 72 | return reconcile.Result{}, err 73 | } 74 | } 75 | 76 | if ok := internal.AddSource(source.Name, source.Spec.Implement); !ok { 77 | log.V(utils.Info).Info("source updated", "name", source.Name) 78 | // ignore add error 79 | //return ctrl.Result{}, nil 80 | r.Eventer.Eventf(source, v1.EventTypeNormal, "Update", "source update success") 81 | } else { 82 | log.V(utils.Info).Info("source add success", "name", source.Name) 83 | r.Eventer.Eventf(source, v1.EventTypeNormal, "Add", "source add success") 84 | } 85 | 86 | // Add repo for helm source 87 | if source.Spec.Type == "helm" && source.Spec.Implement.Helm != nil && source.Spec.Implement.Helm.Repositories != nil { 88 | for _, repo := range source.Spec.Implement.Helm.Repositories { 89 | err := helmsdk.Add(repo.Name, repo.Url, repo.UserName, repo.PassWord, log) 90 | if err != nil { 91 | log.Error(err, "helm repo add failed") 92 | r.Eventer.Eventf(source, v1.EventTypeWarning, "helm repo add failed", "error:%v", err) 93 | return reconcile.Result{}, err 94 | } 95 | } 96 | } 97 | 98 | if !source.Status.Ready { 99 | source.Status.Ready = true 100 | if err := r.Update(ctx, source); err != nil { 101 | log.Error(err, "update source status failed", "name", source.Name) 102 | return ctrl.Result{}, err 103 | } 104 | } 105 | 106 | return ctrl.Result{}, nil 107 | } 108 | 109 | func (r *SourceReconciler) SetupWithManager(mgr ctrl.Manager) error { 110 | return ctrl.NewControllerManagedBy(mgr). 111 | For(&clmv1beta1.Source{}). 112 | Complete(r) 113 | } 114 | 115 | func (r *SourceReconciler) finalizeSource(reqLogger logr.Logger, instance *clmv1beta1.Source) error { 116 | reqLogger.V(utils.Debug).Info("source finalizer", "name", instance.Name) 117 | ok := internal.DeleteSource(instance.Name) 118 | if !ok { 119 | reqLogger.V(utils.Warn).Info("source delete failed") 120 | } 121 | instance.SetFinalizers(utils.Remove(instance.GetFinalizers(), sourceFinalizer)) 122 | if err := r.Update(context.TODO(), instance); err != nil { 123 | reqLogger.Error(err, "failed to update ecs in finalizer", "name", instance.Name) 124 | return err 125 | } 126 | return nil 127 | } 128 | 129 | func (r *SourceReconciler) addFinalizer(reqLogger logr.Logger, instance *clmv1beta1.Source) error { 130 | finalizers := instance.GetFinalizers() 131 | finalizers = append(finalizers, sourceFinalizer) 132 | reqLogger.V(utils.Info).Info("Adding Finalizer for the feature", "feature", instance.Name) 133 | instance.SetFinalizers(finalizers) 134 | return nil 135 | } 136 | -------------------------------------------------------------------------------- /controllers/suite_test.go: -------------------------------------------------------------------------------- 1 | /* 2 | 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 | clmv1beta1 "cloudnativeapp/clm/api/v1beta1" 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 = clmv1beta1.AddToScheme(scheme.Scheme) 66 | Expect(err).NotTo(HaveOccurred()) 67 | 68 | err = clmv1beta1.AddToScheme(scheme.Scheme) 69 | Expect(err).NotTo(HaveOccurred()) 70 | 71 | // +kubebuilder:scaffold:scheme 72 | 73 | k8sClient, err = client.New(cfg, client.Options{Scheme: scheme.Scheme}) 74 | Expect(err).ToNot(HaveOccurred()) 75 | Expect(k8sClient).ToNot(BeNil()) 76 | 77 | close(done) 78 | }, 60) 79 | 80 | var _ = AfterSuite(func() { 81 | By("tearing down the test environment") 82 | err := testEnv.Stop() 83 | Expect(err).ToNot(HaveOccurred()) 84 | }) 85 | -------------------------------------------------------------------------------- /docs/crdrelease.md: -------------------------------------------------------------------------------- 1 | # CRDRelease 2 | 3 | ## Typical CRDRelease File 4 | 5 | ```$xslt 6 | apiVersion: clm.cloudnativeapp.io/v1beta1 7 | kind: CRDRelease 8 | metadata: 9 | name: applications.oam-domain.alibabacloud.com ### crd release name 10 | spec: 11 | version: 1.0.0 ### crd release version 12 | dependencies: ### crd release dependencies 13 | - name: applicationconfigurations.core.oam.dev 14 | version: 1.0.0 15 | strategy: WaitIfAbsent 16 | modules: ### crd release modules 17 | - name: applications-crd 18 | source: 19 | name: native-source 20 | values: 21 | urls: 22 | - https://.../applications-crd.yaml 23 | - name: appmodel-module 24 | precheck: 25 | resourceExist: 26 | - type: CustomResourceDefinition 27 | name: applicationconfigurations.core.oam.dev 28 | recover: 29 | retry: true 30 | readiness: 31 | failureThreshold: 3 32 | periodSeconds: 10 33 | recoverThreshold: 120 34 | httpGet: 35 | path: plugin/module?name=configuration-module&crds=applications.oam-domain.alibabacloud.com 36 | host: adapter-slb 37 | port: 80 38 | scheme: http 39 | source: 40 | name: service-source 41 | values: 42 | container: | 43 | command: 44 | - /manager 45 | - -metrics-addr 46 | - 127.0.0.1:8082 47 | image: xxx:latest 48 | imagePullPolicy: Always 49 | volumeMounts: 50 | - mountPath: /home/admin/ 51 | name: ack-config 52 | readOnly: true 53 | - mountPath: /var/log/edas 54 | name: edas-log 55 | name: configuration 56 | 57 | status: ### crd release status 58 | conditions: 59 | - lastTransitionTime: "2021-02-07T14:44:16Z" 60 | status: "True" 61 | type: Initialized 62 | - lastTransitionTime: "2021-02-07T14:44:16Z" 63 | status: "True" 64 | type: DependenciesSatisfied 65 | - lastTransitionTime: "2021-02-07T14:44:43Z" 66 | status: "True" 67 | type: ModulesReady 68 | - lastTransitionTime: "2021-02-07T14:45:03Z" 69 | status: "True" 70 | type: Ready 71 | currentVersion: 1.0.0 72 | phase: Running 73 | dependencies: 74 | - name: applicationconfigurations.core.oam.dev 75 | phase: Running 76 | version: 1.0.0 77 | modules: 78 | - conditions: 79 | - lastTransitionTime: "2021-02-07T14:44:43Z" 80 | status: "True" 81 | type: Initialized 82 | - lastTransitionTime: "2021-02-07T14:44:43Z" 83 | status: "True" 84 | type: PreChecked 85 | - lastTransitionTime: "2021-02-07T14:44:43Z" 86 | status: "True" 87 | type: SourceReady 88 | - lastTransitionTime: "2021-02-07T14:44:43Z" 89 | status: "True" 90 | type: Ready 91 | lastState: 92 | installing: 93 | startedAt: "2021-02-07T14:44:43Z" 94 | name: applications-crd 95 | ready: true 96 | state: 97 | running: 98 | startedAt: "2021-02-07T14:45:03Z" 99 | ``` 100 | 101 | ### CRDRelease Dependencies 102 | 103 | ```$xslt 104 | dependencies: 105 | - name: applicationconfigurations.core.oam.dev 106 | version: 1.0.0 107 | strategy: WaitIfAbsent (PullIfAbsent | WaitIfAbsent| ErrIfAbsent) 108 | ``` 109 | * strategy : Strategy when dependency not found in cluster. 110 | * PullIfAbsent: Pull dependency from registry when it not found in cluster, error will be throw when pull failed. 111 | * WaitIfAbsent: Default strategy. CRDRelease will wait until dependency appears. 112 | * ErrIfAbsent: Throw an error simply. 113 | 114 | ### CRDRelease Module 115 | ```$xslt 116 | - name: appmodel-module 117 | conditions: ### module conditions 118 | resourceExist: 119 | - type: CustomResourceDefinition 120 | name: applicationconfigurations.core.oam.dev 121 | preCheck: ### module precheck 122 | resourceExist: 123 | - type: CustomResourceDefinition 124 | name: applicationconfigurations.core.oam.dev 125 | recover: ### module recover 126 | retry: true 127 | readiness: ### module readiness 128 | failureThreshold: 3 129 | periodSeconds: 10 130 | recoverThreshold: 120 131 | httpGet: 132 | path: plugin/module?name=configuration-module&crds=applications.oam-domain.alibabacloud.com 133 | host: adapter-slb 134 | port: 80 135 | scheme: http 136 | source: ### module source 137 | name: service-source 138 | ``` 139 | * conditions: Condition check of the module. When condition check failed, module will not be managed by CLM. 140 | * ResourceNotExist: All resources should not exist. 141 | * ResourceExist: All resources should exist. 142 | * Both ResourceNotExist and ResourceExist should meets. 143 | 144 | * preCheck: Check before do crd release installation from source, the installation blocks until check success. 145 | * ResourceNotExist: All resources should not exist. 146 | * ResourceExist: All resources should exist. 147 | * Both ResourceNotExist and ResourceExist should meets. 148 | 149 | * source: See `helm-source`, `native-source`, `service-source` 150 | 151 | * readiness: Readiness prober after module installs successfully, the probe result will change the status of module. 152 | * recoverThreshold: The failed probe result threshold of turning a module status from recover to abnormal. 153 | * successThreshold: The success probe result threshold of turning a module status to running. 154 | * failureThreshold: The failed probe result threshold of turning a module status to abnormal. 155 | * periodSeconds: Probe periods. 156 | * timeoutSeconds: Probe timeOut seconds. 157 | * httpGet/tcpSocket: Please see pkg/probe/probe.go 158 | 159 | * recover: Indicates whether and how to do source recovery. 160 | * retry: Indicates whether retry recover work, request to source to do recovery action. 161 | 162 | 163 | ### CRDRelease Status 164 | 165 | * conditions: Conditions of CRD Release. 166 | * Initialized: Start handle the crd release by clm. 167 | * DependenciesSatisfied: All dependencies are satisfied, and begin to install modules. 168 | * ModulesReady: All modules are ready to work. 169 | * Ready: It means crd release is ready to work now. 170 | 171 | * phase: 172 | * Running. 173 | * Abnormal. 174 | * Installing. 175 | 176 | * dependencies: Dependencies status of CRD Release. Phase below: 177 | * Pulling: Pulling dependency from registry. 178 | * Waiting: Waiting for dependency to be installed successfully. 179 | * AbsentError: Error when strategy is ErrorIfAbsent. 180 | * PullError: Pull dependency from registry error. 181 | * Running: Only when dependency CRDRelease phase is running. 182 | * Abnormal: Dependency phase abnormal. 183 | 184 | * events: Events list of handle CRD Release. 185 | ``` 186 | Type Reason Age From Message 187 | │ ---- ------ ---- ---- ------- 188 | │ Normal Initialized 93s CRDRelease True 189 | │ Normal DependenciesSatisfied 93s CRDRelease True 190 | │ Normal Module:Initialized 93s CRDRelease module nginx.module condition True 191 | │ Normal Module:PreChecked 93s CRDRelease module nginx.module condition True 192 | │ Normal nginx.module:Installing 86s CRDRelease message: reason: 193 | │ Normal ModulesReady 86s CRDRelease False 194 | │ Normal nginx.module:Running 86s CRDRelease 195 | │ Normal ModulesReady 86s CRDRelease True 196 | │ Normal Ready 86s CRDRelease True 197 | 198 | ``` 199 | 200 | ### Manage Existing Resources 201 | Using native source: 202 | * `cd config/sample/import; kubectl apply -f nginx-deployment.yaml` Install nginx deployment. 203 | 204 | * `kubectl apply -f native-import-example.yaml` Install CRD Release to K8s cluster. 205 | 206 | * `kubectl describe crdrelease test-native-import` Check CRD Release status: Status->Modules->Conditions->Type:Imported. 207 | 208 | * `kubectl edit crdrelease test-native-import` Scale deployment replicas to 1 and check deployment status. 209 | 210 | Using helm source: 211 | 212 | * `helm install bitnginx bitnami/nginx` Install helm charts to K8s cluster. 213 | 214 | * `kubectl apply -f helm-import-example.yaml` Install CRD Release to K8s cluster. 215 | 216 | * `kubectl describe crdrelease test-helm-import` Check CRD Release status: Status->Modules->Conditions->Type:Imported. 217 | 218 | * `kubectl delete crdrelease test-helm-import` Delete CRD Release and check result using `helm ls -A`. 219 | 220 | 221 | ### Upgrade CRD Release 222 | 223 | * `cd config/sample/crdrelease; kubectl apply -f native-example.yaml` Install native CRD Release. 224 | 225 | * `kubectl edit crdrelease test-native` Scale deployment replicas to 1 and check deployment status. 226 | 227 | -------------------------------------------------------------------------------- /docs/helm-source.md: -------------------------------------------------------------------------------- 1 | # Helm Source 2 | 3 | ## Source Definition 4 | 5 | ``` 6 | apiVersion: clm.cloudnativeapp.io/v1beta1 7 | kind: Source 8 | metadata: 9 | name: helm-source 10 | spec: 11 | type: helm 12 | implement: 13 | helm: 14 | wait: true ### Whether wait helm action result. 15 | timeout: 120 ### Timeout for waiting. 16 | ``` 17 | 18 | ## Usage In CRDRelease 19 | 20 | ``` 21 | apiVersion: clm.cloudnativeapp.io/v1beta1 22 | kind: CRDRelease 23 | metadata: 24 | name: test-helm 25 | spec: 26 | version: 1.0.0 27 | modules: 28 | - name: nginx.module 29 | source: 30 | name: helm-source 31 | values: 32 | chartPath: "https://cloudnativeapp.oss-cn-shenzhen.aliyuncs.com/clm/nginx-ingress-0.7.1.tgz" ### Helm package URL. 33 | namespace: default ### The namespace to install helm package. 34 | releaseName: nginx ### Helm release name . 35 | chartValues: {"imageUrl":"...","imageTag":"1.0.0"} ### Helm package charts value. 36 | ``` 37 | 38 | ## Support Helm Repository 39 | 40 | ``` 41 | apiVersion: clm.cloudnativeapp.io/v1beta1 42 | kind: Source 43 | metadata: 44 | name: helm-source 45 | spec: 46 | type: helm 47 | implement: 48 | helm: 49 | wait: true 50 | timeout: 120 51 | repositories: 52 | - name: bitnami ### Localname 53 | url: https://charts.bitnami.com/bitnami ### Repository url 54 | - name: nginx-stable 55 | url: https://helm.nginx.com/stable 56 | - name: private 57 | url: https://xxx 58 | username: yourname ### Username of private repository 59 | password: yourpasswd ### Password of private repository 60 | ``` 61 | 62 | ## Usage In CRDRelease 63 | ``` 64 | apiVersion: clm.cloudnativeapp.io/v1beta1 65 | kind: CRDRelease 66 | metadata: 67 | name: test-helm-repo 68 | spec: 69 | version: 1.0.0 70 | modules: 71 | - name: nginx.module 72 | source: 73 | name: helm-source 74 | values: 75 | chartPath: "bitnami/nginx" 76 | namespace: default 77 | releaseName: bitnginx 78 | ``` -------------------------------------------------------------------------------- /docs/native-source.md: -------------------------------------------------------------------------------- 1 | # Native Source 2 | 3 | ## Source Definition 4 | 5 | ``` 6 | apiVersion: clm.cloudnativeapp.io/v1beta1 7 | kind: Source 8 | metadata: 9 | name: native-source 10 | spec: 11 | type: native 12 | implement: 13 | native: 14 | ignoreError: false ### Whether ignore single error when handle multiple resource. 15 | ``` 16 | 17 | ## Usage In CRDRelease 18 | 19 | ``` 20 | apiVersion: clm.cloudnativeapp.io/v1beta1 21 | kind: CRDRelease 22 | metadata: 23 | name: test-native 24 | spec: 25 | version: 1.0.0 26 | modules: 27 | - name: native.module 28 | source: 29 | name: native-source 30 | values: 31 | urls: 32 | - https://cloudnativeapp.oss-cn-shenzhen.aliyuncs.com/clm/scaledobjects-crd.yaml 33 | yaml: | 34 | apiVersion: apps/v1 35 | kind: Deployment 36 | metadata: 37 | name: nginx-deployment 38 | namespace: default 39 | spec: 40 | selector: 41 | matchLabels: 42 | app: nginx 43 | replicas: 2 44 | template: 45 | metadata: 46 | labels: 47 | app: nginx 48 | spec: 49 | containers: 50 | - name: nginx 51 | image: nginx:1.14.2 52 | ports: 53 | - containerPort: 80 54 | ``` 55 | * urls: remote resource file. 56 | * yaml: yaml file to apply directly. 57 | 58 | ## TODO 59 | -------------------------------------------------------------------------------- /docs/service-source.md: -------------------------------------------------------------------------------- 1 | # Service Source 2 | 3 | Service Source means CLM find a K8s service to do lifecycle management. 4 | 5 | ## Source Definition 6 | 7 | ``` 8 | apiVersion: clm.cloudnativeapp.io/v1beta1 9 | kind: Source 10 | metadata: 11 | name: service-source 12 | spec: 13 | type: service 14 | implement: 15 | localService: 16 | name: adapter-slb ### Find a K8s Service named adapter-slb. 17 | namespace: edas-oam-system ### The namespace of K8s Service. 18 | install: ### Install action. 19 | relativePath: plugin/module ### Relative path of install action request. 20 | values: ### Parameter of request. 21 | action: 22 | - install 23 | method: post ### Method of request. 24 | uninstall: 25 | relativePath: plugin/module 26 | values: 27 | action: 28 | - uninstall 29 | method: delete 30 | upgrade: 31 | relativePath: plugin/module 32 | values: 33 | action: 34 | - upgrade 35 | method: post 36 | recover: 37 | relativePath: plugin/module 38 | values: 39 | action: 40 | - recover 41 | method: post 42 | status: 43 | relativePath: plugin/module 44 | values: 45 | action: 46 | - status 47 | method: get 48 | ``` 49 | 50 | ## Usage In CRDRelease 51 | 52 | ``` 53 | apiVersion: clm.cloudnativeapp.io/v1beta1 54 | kind: CRDRelease 55 | metadata: 56 | name: test-service 57 | spec: 58 | version: 1.0.0 59 | modules: 60 | - name: service.module 61 | source: 62 | name: service-source 63 | values: ### Any values can be handle by K8s Service pods 64 | ... 65 | ``` 66 | 67 | ## TODO 68 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module cloudnativeapp/clm 2 | 3 | go 1.13 4 | 5 | require ( 6 | github.com/go-logr/logr v0.2.1 7 | github.com/go-logr/zapr v0.2.0 // indirect 8 | github.com/gofrs/flock v0.8.0 9 | github.com/jonboulle/clockwork v0.1.0 10 | github.com/onsi/ginkgo v1.14.1 11 | github.com/onsi/gomega v1.10.2 12 | github.com/pkg/errors v0.9.1 13 | go.uber.org/zap v1.13.0 14 | gopkg.in/natefinch/lumberjack.v2 v2.0.0 15 | helm.sh/helm/v3 v3.4.1 16 | k8s.io/api v0.19.4 17 | k8s.io/apiextensions-apiserver v0.19.3 18 | k8s.io/apimachinery v0.19.4 19 | k8s.io/cli-runtime v0.19.4 20 | k8s.io/client-go v0.19.4 21 | k8s.io/component-base v0.19.4 22 | k8s.io/klog/v2 v2.4.0 23 | k8s.io/kube-openapi v0.0.0-20200805222855-6aeccd4b50c6 24 | k8s.io/kubectl v0.19.4 25 | k8s.io/utils v0.0.0-20200729134348-d5654de09c73 26 | rsc.io/letsencrypt v0.0.3 // indirect 27 | sigs.k8s.io/controller-runtime v0.6.3 28 | sigs.k8s.io/yaml v1.2.0 29 | ) 30 | -------------------------------------------------------------------------------- /hack/boilerplate.go.txt: -------------------------------------------------------------------------------- 1 | /* 2 | 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 | */ -------------------------------------------------------------------------------- /images/architecture.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cloudnativeapp/CLM/3fdbac85635ac097cee18db44e32e1b9dc971047/images/architecture.jpg -------------------------------------------------------------------------------- /images/crd.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cloudnativeapp/CLM/3fdbac85635ac097cee18db44e32e1b9dc971047/images/crd.jpg -------------------------------------------------------------------------------- /images/crdreleases.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cloudnativeapp/CLM/3fdbac85635ac097cee18db44e32e1b9dc971047/images/crdreleases.jpg -------------------------------------------------------------------------------- /images/helm.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cloudnativeapp/CLM/3fdbac85635ac097cee18db44e32e1b9dc971047/images/helm.jpg -------------------------------------------------------------------------------- /images/module-source.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cloudnativeapp/CLM/3fdbac85635ac097cee18db44e32e1b9dc971047/images/module-source.jpg -------------------------------------------------------------------------------- /images/nginx-deploy.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cloudnativeapp/CLM/3fdbac85635ac097cee18db44e32e1b9dc971047/images/nginx-deploy.jpg -------------------------------------------------------------------------------- /internal/crdrelease_svc.go: -------------------------------------------------------------------------------- 1 | package internal 2 | 3 | import ( 4 | "cloudnativeapp/clm/pkg/utils" 5 | "k8s.io/apiextensions-apiserver/pkg/apis/apiextensions" 6 | v1 "k8s.io/apimachinery/pkg/apis/meta/v1" 7 | ctrl "sigs.k8s.io/controller-runtime" 8 | "sync" 9 | ) 10 | 11 | type CRDReleasePhase string 12 | 13 | const ( 14 | // CRDReleaseRunning means all dependencies have been met and all modules are ready 15 | CRDReleaseRunning CRDReleasePhase = "Running" 16 | CRDReleaseAbnormal CRDReleasePhase = "Abnormal" 17 | CRDReleaseInstalling CRDReleasePhase = "Installing" 18 | ) 19 | 20 | type CRDReleaseCondition struct { 21 | Type CRDReleaseConditionType `json:"type,omitempty"` 22 | Status apiextensions.ConditionStatus `json:"status,omitempty"` 23 | // Last time the condition transitioned from one status to another. 24 | // +optional 25 | LastTransitionTime v1.Time `json:"lastTransitionTime,omitempty" protobuf:"bytes,4,opt,name=lastTransitionTime"` 26 | } 27 | 28 | type CRDReleaseConditionType string 29 | 30 | const ( 31 | // Start handle the crd release by clm. 32 | CRDReleaseInitialized CRDReleaseConditionType = "Initialized" 33 | // All dependencies are satisfied, and begin to install modules. 34 | CRDReleasesDependenciesSatisfied CRDReleaseConditionType = "DependenciesSatisfied" 35 | // All modules are ready to work. 36 | CRDReleaseModulesReady CRDReleaseConditionType = "ModulesReady" 37 | // It means crd release is ready to work now. 38 | CRDReleaseReady CRDReleaseConditionType = "Ready" 39 | ) 40 | 41 | var log = ctrl.Log.WithName("crd release status") 42 | 43 | var statusRecorder = struct { 44 | m map[string]interface{} 45 | sync.RWMutex 46 | }{ 47 | m: make(map[string]interface{}), 48 | } 49 | 50 | //RecordStatus record crd release status to clm memory. 51 | func RecordStatus(name, version string, s interface{}) bool { 52 | defer statusRecorder.Unlock() 53 | statusRecorder.Lock() 54 | key := name + ":" + version 55 | if _, ok := statusRecorder.m[key]; ok { 56 | log.V(utils.Info).Info("crd release status exist already", "name", name) 57 | return false 58 | } 59 | statusRecorder.m[key] = s 60 | return true 61 | } 62 | 63 | //DeleteStatus delete crd release status from clm. 64 | func DeleteStatus(name, version string) bool { 65 | defer statusRecorder.Unlock() 66 | statusRecorder.Lock() 67 | key := name + ":" + version 68 | if _, ok := statusRecorder.m[key]; !ok { 69 | log.V(utils.Info).Info("crd release status does not exist", "name", name) 70 | return false 71 | } 72 | delete(statusRecorder.m, key) 73 | return true 74 | } 75 | 76 | //GetStatus get the crd release status from clm. 77 | func GetStatus(name, version string) (interface{}, bool) { 78 | defer statusRecorder.RUnlock() 79 | statusRecorder.RLock() 80 | key := name + ":" + version 81 | if s, ok := statusRecorder.m[key]; !ok { 82 | log.V(utils.Info).Info("crd release status does not exist", "name", name) 83 | return nil, false 84 | } else { 85 | return s, true 86 | } 87 | } 88 | -------------------------------------------------------------------------------- /internal/dependency_svc.go: -------------------------------------------------------------------------------- 1 | package internal 2 | 3 | import ( 4 | "cloudnativeapp/clm/pkg/utils" 5 | "errors" 6 | "reflect" 7 | ctrl "sigs.k8s.io/controller-runtime" 8 | ) 9 | 10 | type Dependency struct { 11 | Name string `json:"name"` 12 | Version string `json:"version"` 13 | // Strategy when dependency not found in cluster. 14 | Strategy DependencyStrategy `json:"strategy,omitempty"` 15 | // http://example.com/v1/{namespace}/{name}/{version}/content 16 | Registry Registry `json:"registry,omitempty"` 17 | } 18 | 19 | //type Version struct { 20 | // Min string `json:"min,omitempty"` 21 | // Max string `json:"max,omitempty"` 22 | //} 23 | 24 | type DependencyStatus struct { 25 | Name string `json:"name"` 26 | Version string `json:"version"` 27 | // Only when dependency is ready the CRDRelease will continue its installation. 28 | Phase DependencyPhase `json:"phase,omitempty"` 29 | Reason string `json:"reason,omitempty"` 30 | } 31 | 32 | type DependencyStrategy string 33 | 34 | const ( 35 | // Pull dependency from registry when it not found in cluster, error will be throw when pull failed. 36 | PullIfAbsent DependencyStrategy = "PullIfAbsent" 37 | // Default strategy. CRDRelease will wait until dependency appears. 38 | WaitIfAbsent DependencyStrategy = "WaitIfAbsent" 39 | // Throw an error simply. 40 | ErrorIfAbsent DependencyStrategy = "ErrIfAbsent" 41 | ) 42 | 43 | type DependencyPhase string 44 | 45 | const ( 46 | // Pulling dependency from registry. 47 | DependencyPulling DependencyPhase = "Pulling" 48 | // Waiting for dependency to be installed successfully. 49 | DependencyWaiting DependencyPhase = "Waiting" 50 | // Error when strategy is ErrorIfAbsent. 51 | DependencyAbsentErr DependencyPhase = "AbsentError" 52 | // Pull dependency from registry error. 53 | DependencyPullingErr DependencyPhase = "PullError" 54 | // Only when dependency CRDRelease phase is running. 55 | DependencyRunning DependencyPhase = "Running" 56 | // Dependency phase abnormal. 57 | DependencyAbnormal DependencyPhase = "Abnormal" 58 | // Do not care 59 | DependencyDontCare DependencyPhase = "DependencyDontCare" 60 | ) 61 | 62 | var dLog = ctrl.Log.WithName("dependency") 63 | 64 | //Install do install from registry. 65 | func (d Dependency) Install() (interface{}, error) { 66 | dLog.V(utils.Debug).Info("install dependency", "name", d.Name, "version", d.Version) 67 | switch d.Strategy { 68 | case ErrorIfAbsent: 69 | dLog.V(utils.Warn).Info("dependency strategy is errIfAbsent, throw an error") 70 | return string(DependencyAbsentErr), errors.New(utils.DependencyAbsentError) 71 | case PullIfAbsent: 72 | dLog.V(utils.Debug).Info("dependency strategy is pullIfAbsent") 73 | if reflect.DeepEqual(d.Registry, Registry{}) { 74 | dLog.V(utils.Warn).Info("no crd release registry found") 75 | return string(DependencyPullingErr), errors.New(utils.DependencyPullError) 76 | } 77 | return d.Registry.Pull(d.Name, d.Version) 78 | case WaitIfAbsent: 79 | fallthrough 80 | default: 81 | dLog.V(utils.Warn).Info("dependency strategy is waitIfAbsent") 82 | return string(DependencyWaiting), errors.New(utils.DependencyWaiting) 83 | } 84 | } 85 | 86 | func (d Dependency) Uninstall() (interface{}, error) { 87 | panic("can not uninstall dependency now") 88 | } 89 | 90 | //Attributes return the dependency name and version. 91 | func (d Dependency) Attributes() (name, version string) { 92 | return d.Name, d.Version 93 | } 94 | 95 | // DoRecover : Do nothing. 96 | func (d Dependency) DoRecover() (interface{}, error) { 97 | dLog.V(utils.Debug).Info("can not recover dependency now", "name", d.Name, "version", d.Version) 98 | return "", nil 99 | } 100 | 101 | //DoUpgrade do upgrade from registry. 102 | func (d Dependency) DoUpgrade() (interface{}, error) { 103 | dLog.V(utils.Info).Info("dependency upgrade", "release name", d.Name, "target version", d.Version) 104 | if reflect.DeepEqual(d.Registry, Registry{}) { 105 | dLog.V(utils.Warn).Info("no crd release registry found") 106 | return string(DependencyPullingErr), errors.New(utils.DependencyPullError) 107 | } 108 | return d.Registry.Pull(d.Name, d.Version) 109 | } 110 | 111 | //ConvertStatus : convert crd release phase to dependency status. 112 | func (d Dependency) ConvertStatus(crdReleasePhase string, err error) (interface{}, error) { 113 | dLog.V(utils.Debug).Info("convert dependency phase to status", "phase", crdReleasePhase) 114 | status := DependencyStatus{ 115 | Name: d.Name, 116 | Version: d.Version, 117 | } 118 | if err != nil { 119 | status.Reason = err.Error() 120 | } 121 | 122 | switch crdReleasePhase { 123 | case string(CRDReleaseRunning): 124 | status.Phase = DependencyRunning 125 | case "": 126 | fallthrough 127 | case string(CRDReleaseInstalling): 128 | status.Phase = DependencyWaiting 129 | if len(status.Reason) == 0 { 130 | status.Reason = utils.DependencyWaiting 131 | } 132 | case string(CRDReleaseAbnormal): 133 | fallthrough 134 | default: 135 | status.Phase = DependencyAbnormal 136 | if len(status.Reason) == 0 { 137 | status.Reason = utils.DependencyStateAbnormal 138 | } 139 | } 140 | 141 | return status, nil 142 | } 143 | -------------------------------------------------------------------------------- /internal/readiness_svc.go: -------------------------------------------------------------------------------- 1 | package internal 2 | 3 | import ( 4 | "cloudnativeapp/clm/pkg/probe" 5 | "cloudnativeapp/clm/pkg/prober" 6 | "cloudnativeapp/clm/pkg/utils" 7 | "reflect" 8 | "strings" 9 | "sync" 10 | "time" 11 | ) 12 | 13 | var Prober *prober.Prober 14 | 15 | type ModuleReadinessState struct { 16 | State string 17 | Ch chan bool 18 | } 19 | 20 | var ModuleReadinessStateMap = struct { 21 | m map[string]*ModuleReadinessState 22 | sync.RWMutex 23 | }{ 24 | m: make(map[string]*ModuleReadinessState), 25 | } 26 | 27 | //addModule add the module to clm readiness map. 28 | func addModule(name string) bool { 29 | defer ModuleReadinessStateMap.Unlock() 30 | ModuleReadinessStateMap.Lock() 31 | key := name 32 | if _, ok := ModuleReadinessStateMap.m[key]; ok { 33 | mLog.V(utils.Warn).Info("module exist already", "name", name) 34 | return false 35 | } 36 | ModuleReadinessStateMap.m[key] = &ModuleReadinessState{ 37 | State: ModuleInstalling, 38 | } 39 | return true 40 | } 41 | 42 | //deleteModule delete the module from the clm readiness map. 43 | func deleteModule(name string) bool { 44 | defer ModuleReadinessStateMap.Unlock() 45 | ModuleReadinessStateMap.Lock() 46 | key := name 47 | if v, ok := ModuleReadinessStateMap.m[key]; !ok { 48 | mLog.V(utils.Warn).Info("module does not exist", "name", name) 49 | return false 50 | } else { 51 | if v.Ch != nil { 52 | close(v.Ch) 53 | v.Ch = nil 54 | } 55 | delete(ModuleReadinessStateMap.m, key) 56 | return true 57 | } 58 | } 59 | 60 | //updateModule update the module state to the clm readiness map. 61 | func updateModule(name, s string, ch chan bool) bool { 62 | defer ModuleReadinessStateMap.Unlock() 63 | ModuleReadinessStateMap.Lock() 64 | key := name 65 | if v, ok := ModuleReadinessStateMap.m[key]; !ok { 66 | mLog.V(utils.Warn).Info("module does not exist", "name", name) 67 | return false 68 | } else { 69 | v.State = s 70 | if ch != nil { 71 | if v.Ch != nil && v.Ch != ch { 72 | close(v.Ch) 73 | } 74 | v.Ch = ch 75 | } 76 | mLog.V(utils.Info).Info("update module status", "name", name, "status", v.State) 77 | return true 78 | } 79 | } 80 | 81 | func getModule(name string) string { 82 | defer ModuleReadinessStateMap.RUnlock() 83 | ModuleReadinessStateMap.RLock() 84 | key := name 85 | if v, ok := ModuleReadinessStateMap.m[key]; !ok { 86 | mLog.V(utils.Warn).Info("module does not exist", "name", name) 87 | return "" 88 | } else { 89 | mLog.V(utils.Info).Info("module status got", "name", name, "status", v.State) 90 | return v.State 91 | } 92 | } 93 | 94 | func readinessCheck(ch chan bool, m Module) { 95 | if reflect.DeepEqual(m.Readiness, probe.Probe{}) { 96 | // update to running when no readiness setting. 97 | updateModule(m.Name, ModuleRunning, nil) 98 | return 99 | } 100 | interval := m.Readiness.PeriodSeconds 101 | if interval < 3 { 102 | interval = 3 103 | } 104 | failureThreshold := m.Readiness.FailureThreshold 105 | if failureThreshold < 1 { 106 | failureThreshold = 1 107 | } 108 | successThreshold := m.Readiness.SuccessThreshold 109 | if successThreshold < 1 { 110 | successThreshold = 1 111 | } 112 | recoverThreshold := m.Readiness.RecoverThreshold 113 | if recoverThreshold < 1 { 114 | recoverThreshold = 1 115 | } 116 | 117 | successCount := 0 118 | failureCount := 0 119 | ticker := time.NewTicker(time.Second * time.Duration(interval)) 120 | 121 | for { 122 | select { 123 | case <-ch: 124 | return 125 | case <-ticker.C: 126 | result, out, err := Prober.RunProbeWithRetries(3, &m.Readiness) 127 | mLog.V(utils.Info).Info("readiness output", "out", out) 128 | if err == nil && result == probe.Success { 129 | if strings.ToLower(out) == "ready" { 130 | failureCount = 0 131 | successCount++ 132 | if successCount >= m.Readiness.SuccessThreshold { 133 | updateModule(m.Name, ModuleRunning, nil) 134 | continue 135 | } 136 | } 137 | } 138 | 139 | if err != nil { 140 | mLog.Error(err, "readiness failed", "name", m.Name) 141 | } else { // result != probe.Success 142 | mLog.V(utils.Warn).Info("readiness warning", "name", m.Name) 143 | } 144 | s := getModule(m.Name) 145 | successCount = 0 146 | failureCount++ 147 | if s == ModuleRecovering && failureCount >= recoverThreshold { 148 | mLog.V(utils.Info).Info("readiness update module", "from", s, "to", ModuleAbnormal) 149 | updateModule(m.Name, ModuleAbnormal, nil) 150 | } else if s == ModuleRunning && failureCount >= failureThreshold { 151 | mLog.V(utils.Info).Info("readiness update module", "from", s, "to", ModuleAbnormal) 152 | updateModule(m.Name, ModuleAbnormal, nil) 153 | } 154 | } 155 | } 156 | } 157 | -------------------------------------------------------------------------------- /internal/registry_svc.go: -------------------------------------------------------------------------------- 1 | package internal 2 | 3 | import ( 4 | "bytes" 5 | "cloudnativeapp/clm/pkg/cliruntime" 6 | "cloudnativeapp/clm/pkg/implement/service" 7 | "cloudnativeapp/clm/pkg/utils" 8 | "encoding/json" 9 | "errors" 10 | "fmt" 11 | "io/ioutil" 12 | "sigs.k8s.io/yaml" 13 | "strings" 14 | "text/template" 15 | ) 16 | 17 | type Registry struct { 18 | // Http default 19 | Protocol RegistryProtocol `json:"protocol,omitempty"` 20 | // Host, ip or hostname. 21 | Host string `json:"host"` 22 | // The http path, version/namespace/releaseName/releaseVersion default. 23 | Path string `json:"relativePath,omitempty"` 24 | // Registry version. 25 | Version string `json:"version,omitempty"` 26 | Namespace string `json:"namespace,omitempty"` 27 | // Parameters to render the crd release. 28 | Params map[string]string `json:"renderParams,omitempty"` 29 | } 30 | 31 | type RegistryProtocol string 32 | 33 | const ( 34 | Http RegistryProtocol = "http" 35 | Https RegistryProtocol = "https" 36 | ) 37 | 38 | //Pull pull crd release from registry and apply with cli-runtime. 39 | func (r Registry) Pull(name, version string) (interface{}, error) { 40 | dLog.V(utils.Debug).Info("start pull crd release from registry", "name", name, "version", version) 41 | ip, port, err := decodeHost(r.Host) 42 | if err != nil { 43 | dLog.Error(err, "decode host failed", "host", r.Host) 44 | return string(DependencyPullingErr), err 45 | } 46 | var path string 47 | if len(r.Path) > 0 { 48 | path, err = render("path", r.Path, r.Params) 49 | if err != nil { 50 | dLog.Error(err, "render path failed", "path", r.Path, "params", r.Params) 51 | return string(DependencyPullingErr), err 52 | } 53 | 54 | path = fmt.Sprintf("http://%s:%s/%s", ip, port, path) 55 | } else { 56 | path = fmt.Sprintf("http://%s:%s/%s/%s/%s/%s", ip, port, 57 | r.Version, r.Namespace, name, version) 58 | } 59 | 60 | if resp, err := do(path, r.Params); err != nil { 61 | dLog.Error(err, fmt.Sprintf("pull error, ip:%s", ip)) 62 | return string(DependencyPullingErr), err 63 | } else { 64 | crdrelease, err := render(name, resp, r.Params) 65 | if err != nil { 66 | dLog.Error(err, "render crd release failed") 67 | return string(DependencyPullingErr), err 68 | } 69 | dLog.V(utils.Debug).Info("pull crd release", "content", crdrelease) 70 | y, err := yaml.JSONToYAML([]byte(crdrelease)) 71 | if err != nil { 72 | dLog.Error(err, "convert json to yaml failed") 73 | return string(DependencyPullingErr), err 74 | } 75 | // 使用create接口 76 | n := cliruntime.NewApplyOptions(nil, string(y), false) 77 | err = n.Run() 78 | if err != nil { 79 | dLog.Error(err, "native apply error") 80 | return string(DependencyPullingErr), err 81 | } 82 | return string(DependencyPulling), nil 83 | } 84 | } 85 | 86 | func render(name, input string, params map[string]string) (string, error) { 87 | dLog.V(utils.Debug).Info("start render object", "name", name, "input", input, "params", params) 88 | if params == nil || len(params) == 0 { 89 | dLog.V(utils.Debug).Info("do not need to render") 90 | return input, nil 91 | } 92 | tmpl, err := template.New(name).Parse(input) 93 | if err != nil { 94 | dLog.Error(err, "template parse failed") 95 | return "", err 96 | } 97 | buf := new(bytes.Buffer) 98 | err = tmpl.Execute(buf, params) 99 | if err != nil { 100 | dLog.Error(err, "template exec failed") 101 | return "", err 102 | } 103 | return buf.String(), nil 104 | } 105 | 106 | func do(path string, params map[string]string) (string, error) { 107 | dLog.V(utils.Debug).Info("do pull crd release", "path", path, "params", params) 108 | if resp, err := service.Do("get", path, nil, nil); err != nil { 109 | dLog.Error(err, "http get error") 110 | return "", err 111 | } else { 112 | defer resp.Body.Close() 113 | if content, err := ioutil.ReadAll(resp.Body); err != nil { 114 | dLog.Error(err, "http get read content error") 115 | return "", err 116 | } else { 117 | if result, err := disableEscape(content); err != nil { 118 | dLog.Error(err, "disable escape error") 119 | return "", err 120 | } else { 121 | return result, nil 122 | } 123 | } 124 | } 125 | } 126 | 127 | func disableEscape(input []byte) (string, error) { 128 | data := make(map[string]interface{}) 129 | if err := json.Unmarshal(input, &data); err != nil { 130 | return "", err 131 | } 132 | newStr, err := json.Marshal(data) 133 | if err != nil { 134 | return "", err 135 | } 136 | return string(newStr), err 137 | } 138 | 139 | func decodeHost(host string) (ip, port string, err error) { 140 | str := strings.Split(host, ":") 141 | ip = str[0] 142 | if len(str) > 2 { 143 | err := errors.New("decode host failed") 144 | return ip, port, err 145 | } 146 | if len(str) == 2 { 147 | port = str[1] 148 | } else { 149 | port = "80" 150 | } 151 | return ip, port, err 152 | } 153 | -------------------------------------------------------------------------------- /internal/source_svc.go: -------------------------------------------------------------------------------- 1 | package internal 2 | 3 | import ( 4 | "cloudnativeapp/clm/pkg/implement" 5 | "cloudnativeapp/clm/pkg/utils" 6 | "encoding/json" 7 | "errors" 8 | ctrl "sigs.k8s.io/controller-runtime" 9 | "sync" 10 | ) 11 | 12 | var sLog = ctrl.Log.WithName("source") 13 | 14 | var SourcesRegistered = struct { 15 | m map[string]implement.Implement 16 | sync.RWMutex 17 | }{ 18 | m: make(map[string]implement.Implement), 19 | } 20 | 21 | //AddSource add source to clm memory. 22 | func AddSource(name string, s implement.Implement) bool { 23 | defer SourcesRegistered.Unlock() 24 | SourcesRegistered.Lock() 25 | if _, ok := SourcesRegistered.m[name]; ok { 26 | sLog.V(utils.Info).Info("source exist already", "name", name) 27 | SourcesRegistered.m[name] = s 28 | sLog.V(utils.Info).Info("source implement updated") 29 | return false 30 | } 31 | sLog.V(utils.Info).Info("source add success", "name", name) 32 | SourcesRegistered.m[name] = s 33 | return true 34 | } 35 | 36 | //DeleteSource delete source from clm. 37 | func DeleteSource(name string) bool { 38 | defer SourcesRegistered.Unlock() 39 | SourcesRegistered.Lock() 40 | if _, ok := SourcesRegistered.m[name]; !ok { 41 | sLog.V(utils.Warn).Info("source does not exist", "name", name) 42 | return false 43 | } 44 | sLog.V(utils.Info).Info("source delete success", "name", name) 45 | delete(SourcesRegistered.m, name) 46 | return true 47 | } 48 | 49 | //GetSource get the source configuration from clm. 50 | func GetSource(name string) (implement.Implement, bool) { 51 | defer SourcesRegistered.RUnlock() 52 | SourcesRegistered.RLock() 53 | if s, ok := SourcesRegistered.m[name]; !ok { 54 | sLog.V(utils.Warn).Info("source does not exist", "name", name) 55 | return implement.Implement{}, false 56 | } else { 57 | return s, true 58 | } 59 | } 60 | 61 | func installFromSource(source Source, targetName, targetVersion string) error { 62 | sLog.V(utils.Debug).Info("try to install from source", "source", source, "target name", targetName) 63 | if s, ok := GetSource(source.Name); !ok { 64 | err := errors.New(utils.ImplementNotFound) 65 | sLog.Error(err, "can not find source", "sourceName", source.Name) 66 | return err 67 | } else { 68 | var values map[string]interface{} 69 | if source.Values != nil && len(source.Values.Raw) != 0 { 70 | if err := json.Unmarshal(source.Values.Raw, &values); err != nil { 71 | sLog.Error(err, "can not unmarshal source value", "sourceName", source.Name) 72 | return err 73 | } 74 | } 75 | if err := s.Install(targetName, targetVersion, values); err != nil { 76 | sLog.Error(err, "install by implement failed", "sourceName", source.Name) 77 | return err 78 | } else { 79 | return nil 80 | } 81 | } 82 | } 83 | 84 | func uninstallFromSource(source Source, targetName, targetVersion string) error { 85 | sLog.V(utils.Debug).Info("try to uninstall from source", "source", source, "target name", targetName) 86 | if s, ok := GetSource(source.Name); !ok { 87 | err := errors.New(utils.ImplementNotFound) 88 | sLog.Error(err, "can not find source", "sourceName", source.Name) 89 | return err 90 | } else { 91 | var values map[string]interface{} 92 | if source.Values != nil && len(source.Values.Raw) != 0 { 93 | if err := json.Unmarshal(source.Values.Raw, &values); err != nil { 94 | sLog.Error(err, "can not unmarshal source value", "sourceName", source.Name) 95 | return err 96 | } 97 | } 98 | if err := s.Uninstall(targetName, targetVersion, values); err != nil { 99 | return err 100 | } else { 101 | return nil 102 | } 103 | } 104 | } 105 | 106 | func recoverFromSource(source Source, targetName, targetVersion string) error { 107 | sLog.V(utils.Debug).Info("try to recover from source", "source", source, "target name", targetName) 108 | if s, ok := GetSource(source.Name); !ok { 109 | err := errors.New(utils.ImplementNotFound) 110 | sLog.Error(err, "can not find source", "sourceName", source.Name) 111 | return err 112 | } else { 113 | var values map[string]interface{} 114 | if source.Values != nil && len(source.Values.Raw) != 0 { 115 | if err := json.Unmarshal(source.Values.Raw, &values); err != nil { 116 | sLog.Error(err, "can not unmarshal source value", "sourceName", source.Name) 117 | return err 118 | } 119 | } 120 | if err := s.Recover(targetName, targetVersion, values); err != nil { 121 | sLog.Error(err, "recover by implement failed", "sourceName", source.Name) 122 | return err 123 | } else { 124 | return nil 125 | } 126 | } 127 | } 128 | 129 | func upgradeFromSource(source Source, targetName, targetVersion string) error { 130 | sLog.V(utils.Debug).Info("try to upgrade from source", "source", source, "target name", targetName) 131 | if s, ok := GetSource(source.Name); !ok { 132 | err := errors.New(utils.ImplementNotFound) 133 | sLog.Error(err, "can not find source", "sourceName", source.Name) 134 | return err 135 | } else { 136 | var values map[string]interface{} 137 | if source.Values != nil && len(source.Values.Raw) != 0 { 138 | if err := json.Unmarshal(source.Values.Raw, &values); err != nil { 139 | sLog.Error(err, "can not unmarshal source value", "sourceName", source.Name) 140 | return err 141 | } 142 | } 143 | if err := s.Upgrade(targetName, targetVersion, values); err != nil { 144 | sLog.Error(err, "upgrade by implement failed", "sourceName", source.Name) 145 | return err 146 | } else { 147 | return nil 148 | } 149 | } 150 | } 151 | -------------------------------------------------------------------------------- /internal/zz_generated.deepcopy.go: -------------------------------------------------------------------------------- 1 | // +build !ignore_autogenerated 2 | 3 | /* 4 | 5 | 6 | Licensed under the Apache License, Version 2.0 (the "License"); 7 | you may not use this file except in compliance with the License. 8 | You may obtain a copy of the License at 9 | 10 | http://www.apache.org/licenses/LICENSE-2.0 11 | 12 | Unless required by applicable law or agreed to in writing, software 13 | distributed under the License is distributed on an "AS IS" BASIS, 14 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 15 | See the License for the specific language governing permissions and 16 | limitations under the License. 17 | */ 18 | 19 | // Code generated by controller-gen. DO NOT EDIT. 20 | 21 | package internal 22 | 23 | import ( 24 | "k8s.io/apimachinery/pkg/runtime" 25 | ) 26 | 27 | // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. 28 | func (in *CRDReleaseCondition) DeepCopyInto(out *CRDReleaseCondition) { 29 | *out = *in 30 | in.LastTransitionTime.DeepCopyInto(&out.LastTransitionTime) 31 | } 32 | 33 | // DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new CRDReleaseCondition. 34 | func (in *CRDReleaseCondition) DeepCopy() *CRDReleaseCondition { 35 | if in == nil { 36 | return nil 37 | } 38 | out := new(CRDReleaseCondition) 39 | in.DeepCopyInto(out) 40 | return out 41 | } 42 | 43 | // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. 44 | func (in *Dependency) DeepCopyInto(out *Dependency) { 45 | *out = *in 46 | in.Registry.DeepCopyInto(&out.Registry) 47 | } 48 | 49 | // DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new Dependency. 50 | func (in *Dependency) DeepCopy() *Dependency { 51 | if in == nil { 52 | return nil 53 | } 54 | out := new(Dependency) 55 | in.DeepCopyInto(out) 56 | return out 57 | } 58 | 59 | // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. 60 | func (in *DependencyStatus) DeepCopyInto(out *DependencyStatus) { 61 | *out = *in 62 | } 63 | 64 | // DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new DependencyStatus. 65 | func (in *DependencyStatus) DeepCopy() *DependencyStatus { 66 | if in == nil { 67 | return nil 68 | } 69 | out := new(DependencyStatus) 70 | in.DeepCopyInto(out) 71 | return out 72 | } 73 | 74 | // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. 75 | func (in *Module) DeepCopyInto(out *Module) { 76 | *out = *in 77 | in.Conditions.DeepCopyInto(&out.Conditions) 78 | in.PreCheck.DeepCopyInto(&out.PreCheck) 79 | in.Source.DeepCopyInto(&out.Source) 80 | in.Readiness.DeepCopyInto(&out.Readiness) 81 | out.Recover = in.Recover 82 | } 83 | 84 | // DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new Module. 85 | func (in *Module) DeepCopy() *Module { 86 | if in == nil { 87 | return nil 88 | } 89 | out := new(Module) 90 | in.DeepCopyInto(out) 91 | return out 92 | } 93 | 94 | // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. 95 | func (in *ModuleCondition) DeepCopyInto(out *ModuleCondition) { 96 | *out = *in 97 | in.LastTransitionTime.DeepCopyInto(&out.LastTransitionTime) 98 | } 99 | 100 | // DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new ModuleCondition. 101 | func (in *ModuleCondition) DeepCopy() *ModuleCondition { 102 | if in == nil { 103 | return nil 104 | } 105 | out := new(ModuleCondition) 106 | in.DeepCopyInto(out) 107 | return out 108 | } 109 | 110 | // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. 111 | func (in *ModuleState) DeepCopyInto(out *ModuleState) { 112 | *out = *in 113 | if in.Installing != nil { 114 | in, out := &in.Installing, &out.Installing 115 | *out = new(ModuleStateInternal) 116 | (*in).DeepCopyInto(*out) 117 | } 118 | if in.Running != nil { 119 | in, out := &in.Running, &out.Running 120 | *out = new(ModuleStateInternal) 121 | (*in).DeepCopyInto(*out) 122 | } 123 | if in.Recovering != nil { 124 | in, out := &in.Recovering, &out.Recovering 125 | *out = new(ModuleStateInternal) 126 | (*in).DeepCopyInto(*out) 127 | } 128 | if in.Abnormal != nil { 129 | in, out := &in.Abnormal, &out.Abnormal 130 | *out = new(ModuleStateInternal) 131 | (*in).DeepCopyInto(*out) 132 | } 133 | if in.Terminated != nil { 134 | in, out := &in.Terminated, &out.Terminated 135 | *out = new(ModuleStateInternal) 136 | (*in).DeepCopyInto(*out) 137 | } 138 | } 139 | 140 | // DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new ModuleState. 141 | func (in *ModuleState) DeepCopy() *ModuleState { 142 | if in == nil { 143 | return nil 144 | } 145 | out := new(ModuleState) 146 | in.DeepCopyInto(out) 147 | return out 148 | } 149 | 150 | // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. 151 | func (in *ModuleStateInternal) DeepCopyInto(out *ModuleStateInternal) { 152 | *out = *in 153 | in.StartedAt.DeepCopyInto(&out.StartedAt) 154 | } 155 | 156 | // DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new ModuleStateInternal. 157 | func (in *ModuleStateInternal) DeepCopy() *ModuleStateInternal { 158 | if in == nil { 159 | return nil 160 | } 161 | out := new(ModuleStateInternal) 162 | in.DeepCopyInto(out) 163 | return out 164 | } 165 | 166 | // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. 167 | func (in *ModuleStatus) DeepCopyInto(out *ModuleStatus) { 168 | *out = *in 169 | if in.Conditions != nil { 170 | in, out := &in.Conditions, &out.Conditions 171 | *out = make([]ModuleCondition, len(*in)) 172 | for i := range *in { 173 | (*in)[i].DeepCopyInto(&(*out)[i]) 174 | } 175 | } 176 | if in.State != nil { 177 | in, out := &in.State, &out.State 178 | *out = new(ModuleState) 179 | (*in).DeepCopyInto(*out) 180 | } 181 | if in.LastState != nil { 182 | in, out := &in.LastState, &out.LastState 183 | *out = new(ModuleState) 184 | (*in).DeepCopyInto(*out) 185 | } 186 | } 187 | 188 | // DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new ModuleStatus. 189 | func (in *ModuleStatus) DeepCopy() *ModuleStatus { 190 | if in == nil { 191 | return nil 192 | } 193 | out := new(ModuleStatus) 194 | in.DeepCopyInto(out) 195 | return out 196 | } 197 | 198 | // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. 199 | func (in *Recover) DeepCopyInto(out *Recover) { 200 | *out = *in 201 | out.Recover = in.Recover 202 | } 203 | 204 | // DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new Recover. 205 | func (in *Recover) DeepCopy() *Recover { 206 | if in == nil { 207 | return nil 208 | } 209 | out := new(Recover) 210 | in.DeepCopyInto(out) 211 | return out 212 | } 213 | 214 | // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. 215 | func (in *Registry) DeepCopyInto(out *Registry) { 216 | *out = *in 217 | if in.Params != nil { 218 | in, out := &in.Params, &out.Params 219 | *out = make(map[string]string, len(*in)) 220 | for key, val := range *in { 221 | (*out)[key] = val 222 | } 223 | } 224 | } 225 | 226 | // DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new Registry. 227 | func (in *Registry) DeepCopy() *Registry { 228 | if in == nil { 229 | return nil 230 | } 231 | out := new(Registry) 232 | in.DeepCopyInto(out) 233 | return out 234 | } 235 | 236 | // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. 237 | func (in *Source) DeepCopyInto(out *Source) { 238 | *out = *in 239 | if in.Values != nil { 240 | in, out := &in.Values, &out.Values 241 | *out = new(runtime.RawExtension) 242 | (*in).DeepCopyInto(*out) 243 | } 244 | } 245 | 246 | // DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new Source. 247 | func (in *Source) DeepCopy() *Source { 248 | if in == nil { 249 | return nil 250 | } 251 | out := new(Source) 252 | in.DeepCopyInto(out) 253 | return out 254 | } 255 | -------------------------------------------------------------------------------- /main.go: -------------------------------------------------------------------------------- 1 | /* 2 | 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | 17 | package main 18 | 19 | import ( 20 | "cloudnativeapp/clm/internal" 21 | "cloudnativeapp/clm/pkg/prober" 22 | "flag" 23 | zap1 "go.uber.org/zap" 24 | "go.uber.org/zap/zapcore" 25 | "io" 26 | "os" 27 | "strings" 28 | 29 | "gopkg.in/natefinch/lumberjack.v2" 30 | "k8s.io/apimachinery/pkg/runtime" 31 | clientgoscheme "k8s.io/client-go/kubernetes/scheme" 32 | _ "k8s.io/client-go/plugin/pkg/client/auth/gcp" 33 | ctrl "sigs.k8s.io/controller-runtime" 34 | "sigs.k8s.io/controller-runtime/pkg/log/zap" 35 | 36 | clmv1beta1 "cloudnativeapp/clm/api/v1beta1" 37 | "cloudnativeapp/clm/controllers" 38 | // +kubebuilder:scaffold:imports 39 | ) 40 | 41 | var ( 42 | scheme = runtime.NewScheme() 43 | setupLog = ctrl.Log.WithName("setup") 44 | ) 45 | 46 | func init() { 47 | _ = clientgoscheme.AddToScheme(scheme) 48 | 49 | _ = clmv1beta1.AddToScheme(scheme) 50 | // +kubebuilder:scaffold:scheme 51 | } 52 | 53 | func main() { 54 | var metricsAddr string 55 | var enableLeaderElection bool 56 | var logToFile bool 57 | var logLevel string 58 | var logFilePath string 59 | var logFileMaxSize int 60 | var logFileMaxBackups int 61 | flag.StringVar(&metricsAddr, "metrics-addr", ":8080", "The address the metric endpoint binds to.") 62 | flag.BoolVar(&enableLeaderElection, "enable-leader-election", false, 63 | "Enable leader election for controller manager. "+ 64 | "Enabling this will ensure there is only one active controller manager.") 65 | // logger related setting 66 | flag.BoolVar(&logToFile, "enable-log-file", false, "Enable to write log to file.") 67 | flag.StringVar(&logLevel, "log-level", "info", "The log level. Available: info, debug") 68 | flag.StringVar(&logFilePath, "log-file-path", "/var/log/clm.log", 69 | "The path of log if enable-log-file is true.") 70 | flag.IntVar(&logFileMaxSize, "log-file-maxsize", 200, 71 | "The maxsize of log file if enable-log-file is true.") 72 | flag.IntVar(&logFileMaxBackups, "log-file-maxbackups", 3, 73 | "The max backups of log file if enable-log-file is true.") 74 | 75 | flag.Parse() 76 | 77 | if logToFile { 78 | w := zapcore.AddSync(&lumberjack.Logger{ 79 | Filename: logFilePath, 80 | MaxSize: logFileMaxSize, 81 | MaxBackups: logFileMaxBackups, 82 | }) 83 | mw := io.MultiWriter(w, os.Stdout) 84 | // 生产版本的日志格式有点丑陋 85 | encCfg := zap1.NewDevelopmentEncoderConfig() 86 | encoder := zapcore.NewConsoleEncoder(encCfg) 87 | ctrl.SetLogger(zap.New(zap.UseDevMode(parseLogLevel(logLevel)), zap.WriteTo(mw), zap.Encoder(encoder))) 88 | } else { 89 | ctrl.SetLogger(zap.New(zap.UseDevMode(true))) 90 | } 91 | 92 | mgr, err := ctrl.NewManager(ctrl.GetConfigOrDie(), ctrl.Options{ 93 | Scheme: scheme, 94 | MetricsBindAddress: metricsAddr, 95 | Port: 9443, 96 | LeaderElection: enableLeaderElection, 97 | LeaderElectionID: "b554fb75.cloudnativeapp.io", 98 | }) 99 | if err != nil { 100 | setupLog.Error(err, "unable to start manager") 101 | os.Exit(1) 102 | } 103 | 104 | if err = (&controllers.CRDReleaseReconciler{ 105 | Client: mgr.GetClient(), 106 | Log: ctrl.Log.WithName("controllers").WithName("CRDRelease"), 107 | Scheme: mgr.GetScheme(), 108 | Eventer: mgr.GetEventRecorderFor("CRDRelease"), 109 | }).SetupWithManager(mgr); err != nil { 110 | setupLog.Error(err, "unable to create controller", "controller", "CRDRelease") 111 | os.Exit(1) 112 | } 113 | if err = (&controllers.SourceReconciler{ 114 | Client: mgr.GetClient(), 115 | Log: ctrl.Log.WithName("controllers").WithName("Source"), 116 | Scheme: mgr.GetScheme(), 117 | Eventer: mgr.GetEventRecorderFor("Source"), 118 | }).SetupWithManager(mgr); err != nil { 119 | setupLog.Error(err, "unable to create controller", "controller", "Source") 120 | os.Exit(1) 121 | } 122 | // +kubebuilder:scaffold:builder 123 | 124 | controllers.MGRClient = mgr.GetClient() 125 | internal.Prober = prober.NewProber() 126 | controllers.EventRecorder = mgr.GetEventRecorderFor("CRDRelease") 127 | setupLog.Info("starting manager") 128 | if err := mgr.Start(ctrl.SetupSignalHandler()); err != nil { 129 | setupLog.Error(err, "problem running manager") 130 | os.Exit(1) 131 | } 132 | } 133 | 134 | // Info : only error will print stacktrace, Debug: warn and error will print stacktrace. 135 | func parseLogLevel(level string) bool { 136 | switch strings.ToLower(level) { 137 | case "info": 138 | return false 139 | case "debug": 140 | return true 141 | default: 142 | setupLog.Info("can not find the log level, set it to 'info' level", "target level", level) 143 | return false 144 | } 145 | } 146 | -------------------------------------------------------------------------------- /pkg/check/condition/condition.go: -------------------------------------------------------------------------------- 1 | package condition 2 | 3 | import ( 4 | "cloudnativeapp/clm/pkg/check/resource" 5 | "cloudnativeapp/clm/pkg/utils" 6 | "github.com/go-logr/logr" 7 | "github.com/pkg/errors" 8 | ) 9 | 10 | type Condition struct { 11 | // All resources should not exist. 12 | ResourceNotExist []resource.Resource `json:"resourceNotExist,omitempty"` 13 | // All resources should exist. 14 | ResourceExist []resource.Resource `json:"resourceExist,omitempty"` 15 | // Strategy when condition does not met 16 | Strategy Strategy `json:"strategy,omitempty"` 17 | } 18 | 19 | type Strategy string 20 | 21 | const ( 22 | // When condition check failed, the module can not be managed by clm. 23 | External Strategy = "External" 24 | // When condition check failed, we take it as a ready module. 25 | Import Strategy = "Import" 26 | ) 27 | 28 | //Check do condition check 29 | func (c Condition) Check(log logr.Logger) (bool, error) { 30 | log.V(utils.Debug).Info("try to check condition", "condition", c) 31 | if len(c.ResourceNotExist) != 0 { 32 | for _, r := range c.ResourceNotExist { 33 | if ok, err := r.Check(log, false); err != nil { 34 | log.Error(err, "condition check error") 35 | return false, err 36 | } else if !ok { 37 | log.Error(errors.New("resource exists"), "condition check failed", 38 | "name", r.Name, "type", r.Type) 39 | return false, nil 40 | } 41 | } 42 | } 43 | 44 | if len(c.ResourceExist) != 0 { 45 | for _, r := range c.ResourceExist { 46 | if ok, err := r.Check(log, true); err != nil { 47 | log.Error(err, "condition check error") 48 | return false, err 49 | } else if !ok { 50 | log.Error(errors.New("resource does not exist"), "condition check failed", 51 | "name", r.Name, "type", r.Type) 52 | return false, nil 53 | } 54 | } 55 | } 56 | 57 | return true, nil 58 | } 59 | -------------------------------------------------------------------------------- /pkg/check/condition/zz_generated.deepcopy.go: -------------------------------------------------------------------------------- 1 | // +build !ignore_autogenerated 2 | 3 | /* 4 | 5 | 6 | Licensed under the Apache License, Version 2.0 (the "License"); 7 | you may not use this file except in compliance with the License. 8 | You may obtain a copy of the License at 9 | 10 | http://www.apache.org/licenses/LICENSE-2.0 11 | 12 | Unless required by applicable law or agreed to in writing, software 13 | distributed under the License is distributed on an "AS IS" BASIS, 14 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 15 | See the License for the specific language governing permissions and 16 | limitations under the License. 17 | */ 18 | 19 | // Code generated by controller-gen. DO NOT EDIT. 20 | 21 | package condition 22 | 23 | import ( 24 | "cloudnativeapp/clm/pkg/check/resource" 25 | ) 26 | 27 | // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. 28 | func (in *Condition) DeepCopyInto(out *Condition) { 29 | *out = *in 30 | if in.ResourceNotExist != nil { 31 | in, out := &in.ResourceNotExist, &out.ResourceNotExist 32 | *out = make([]resource.Resource, len(*in)) 33 | copy(*out, *in) 34 | } 35 | if in.ResourceExist != nil { 36 | in, out := &in.ResourceExist, &out.ResourceExist 37 | *out = make([]resource.Resource, len(*in)) 38 | copy(*out, *in) 39 | } 40 | } 41 | 42 | // DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new Condition. 43 | func (in *Condition) DeepCopy() *Condition { 44 | if in == nil { 45 | return nil 46 | } 47 | out := new(Condition) 48 | in.DeepCopyInto(out) 49 | return out 50 | } 51 | -------------------------------------------------------------------------------- /pkg/check/precheck/crdcheck.go: -------------------------------------------------------------------------------- 1 | package precheck 2 | 3 | import ( 4 | "context" 5 | v1 "k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1" 6 | "k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1beta1" 7 | "k8s.io/apimachinery/pkg/runtime" 8 | "log" 9 | ctrl "sigs.k8s.io/controller-runtime" 10 | "sigs.k8s.io/controller-runtime/pkg/client" 11 | ) 12 | 13 | type CRDCheck struct { 14 | Conflict []CRD `json:"conflict,omitempty"` 15 | Required []CRD `json:"required,omitempty"` 16 | } 17 | 18 | type CRD struct { 19 | Name string `json:"name,omitempty"` 20 | Version string `json:"version,omitempty"` 21 | } 22 | 23 | //Check 为true才能继续安装 24 | func (c *CRDCheck) Check() (bool, error) { 25 | scheme := runtime.NewScheme() 26 | k8sclient, err := client.New(ctrl.GetConfigOrDie(), client.Options{Scheme: scheme}) 27 | if err != nil { 28 | log.Printf("new k8s client err %v", err) 29 | return false, err 30 | } 31 | 32 | o := &client.ListOptions{} 33 | var list v1.CustomResourceDefinitionList 34 | v1.AddToScheme(scheme) 35 | if err = k8sclient.List(context.Background(), &list, o); err != nil { 36 | log.Printf("list crd err %v", err) 37 | var list v1beta1.CustomResourceDefinitionList 38 | v1beta1.AddToScheme(scheme) 39 | if err = k8sclient.List(context.Background(), &list, o); err != nil { 40 | log.Printf("list crd err %v", err) 41 | return false, err 42 | } 43 | _, e := doCheckV1beta1(list.Items, c.Conflict, c.Required) 44 | return e, nil 45 | } 46 | _, e := doCheckV1(list.Items, c.Conflict, c.Required) 47 | return e, nil 48 | } 49 | 50 | func doCheckV1beta1(crds []v1beta1.CustomResourceDefinition, conflicts, exists []CRD) (bool, bool) { 51 | conflict := true 52 | exist := true 53 | for _, c := range conflicts { 54 | if v1beta1CRDExist(crds, c.Name, c.Version) { 55 | conflict = false 56 | break 57 | } 58 | } 59 | 60 | for _, c := range exists { 61 | if !v1beta1CRDExist(crds, c.Name, c.Version) { 62 | log.Printf("crd %s is needed", c.Name) 63 | exist = false 64 | break 65 | } 66 | } 67 | return conflict, exist 68 | } 69 | 70 | func doCheckV1(crds []v1.CustomResourceDefinition, conflicts, exists []CRD) (bool, bool) { 71 | conflict := true 72 | exist := true 73 | for _, c := range conflicts { 74 | if v1CRDExist(crds, c.Name, c.Version) { 75 | conflict = false 76 | break 77 | } 78 | } 79 | 80 | for _, c := range exists { 81 | if !v1CRDExist(crds, c.Name, c.Version) { 82 | log.Printf("crd %s is needed", c.Name) 83 | exist = false 84 | break 85 | } 86 | } 87 | return conflict, exist 88 | } 89 | 90 | func v1CRDExist(crds []v1.CustomResourceDefinition, name, version string) bool { 91 | for _, i := range crds { 92 | if i.Name == name { 93 | established := false 94 | namesAccepted := false 95 | for _, c := range i.Status.Conditions { 96 | if c.Type == v1.NamesAccepted && c.Status == v1.ConditionTrue { 97 | log.Printf("crd %s names accepted", name) 98 | namesAccepted = true 99 | } 100 | if c.Type == v1.Established && c.Status == v1.ConditionTrue { 101 | log.Printf("crd %s established", name) 102 | established = true 103 | } 104 | } 105 | if !established || !namesAccepted { 106 | return false 107 | } 108 | if len(version) == 0 { 109 | return true 110 | } else { 111 | for _, v := range i.Spec.Versions { 112 | if v.Name == version && v.Served { 113 | return true 114 | } 115 | } 116 | } 117 | 118 | } 119 | } 120 | return false 121 | } 122 | 123 | func v1beta1CRDExist(crds []v1beta1.CustomResourceDefinition, name, version string) bool { 124 | for _, i := range crds { 125 | if i.Name == name { 126 | established := false 127 | namesAccepted := false 128 | for _, c := range i.Status.Conditions { 129 | if c.Type == v1beta1.Established && c.Status == v1beta1.ConditionTrue { 130 | log.Printf("crd %s established", name) 131 | log.Printf("established time %s", c.LastTransitionTime) 132 | established = true 133 | } 134 | if c.Type == v1beta1.NamesAccepted && c.Status == v1beta1.ConditionTrue { 135 | log.Printf("crd %s names accepted", name) 136 | log.Printf("accepted time %s", c.LastTransitionTime) 137 | namesAccepted = true 138 | } 139 | } 140 | if !established || !namesAccepted { 141 | return false 142 | } 143 | log.Printf("crd create time %v", i.CreationTimestamp) 144 | if len(version) == 0 { 145 | return true 146 | } else if version == i.Spec.Version { 147 | return true 148 | } 149 | } 150 | } 151 | return false 152 | } 153 | -------------------------------------------------------------------------------- /pkg/check/precheck/precheck.go: -------------------------------------------------------------------------------- 1 | package precheck 2 | 3 | import "cloudnativeapp/clm/pkg/check/resource" 4 | 5 | type Precheck struct { 6 | // All resources should not exist. 7 | ResourceNotExist []resource.Resource `json:"resourceNotExist,omitempty"` 8 | // All resources should exist. 9 | ResourceExist []resource.Resource `json:"resourceExist,omitempty"` 10 | } 11 | -------------------------------------------------------------------------------- /pkg/check/resource/resoucecheck.go: -------------------------------------------------------------------------------- 1 | package resource 2 | 3 | import ( 4 | "cloudnativeapp/clm/pkg/cliruntime" 5 | "cloudnativeapp/clm/pkg/utils" 6 | "github.com/go-logr/logr" 7 | "k8s.io/apimachinery/pkg/api/errors" 8 | ) 9 | 10 | type Resource struct { 11 | Type string `json:"type,omitempty"` 12 | Name string `json:"name,omitempty"` 13 | Namespace string `json:"namespace,omitempty"` 14 | } 15 | 16 | //Check Check the resource. 17 | func (res Resource) Check(log logr.Logger, exist bool) (bool, error) { 18 | log.V(utils.Debug).Info("try to check resource", "resource", res, "exist", exist) 19 | allNamespace := false 20 | if len(res.Namespace) == 0 { 21 | allNamespace = true 22 | } 23 | g := cliruntime.NewGetOption(false, res.Namespace, allNamespace) 24 | _, err := g.Run([]string{res.Type, res.Name}) 25 | if err != nil { 26 | log.V(utils.Debug).Info("find resource error", "resource", res, "error", err.Error()) 27 | if errors.IsNotFound(err) && !exist { 28 | return true, nil 29 | } 30 | return false, err 31 | } 32 | if exist { 33 | return true, nil 34 | } 35 | 36 | return false, nil 37 | } 38 | -------------------------------------------------------------------------------- /pkg/check/resource/zz_generated.deepcopy.go: -------------------------------------------------------------------------------- 1 | // +build !ignore_autogenerated 2 | 3 | /* 4 | 5 | 6 | Licensed under the Apache License, Version 2.0 (the "License"); 7 | you may not use this file except in compliance with the License. 8 | You may obtain a copy of the License at 9 | 10 | http://www.apache.org/licenses/LICENSE-2.0 11 | 12 | Unless required by applicable law or agreed to in writing, software 13 | distributed under the License is distributed on an "AS IS" BASIS, 14 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 15 | See the License for the specific language governing permissions and 16 | limitations under the License. 17 | */ 18 | 19 | // Code generated by controller-gen. DO NOT EDIT. 20 | 21 | package resource 22 | 23 | // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. 24 | func (in *Resource) DeepCopyInto(out *Resource) { 25 | *out = *in 26 | } 27 | 28 | // DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new Resource. 29 | func (in *Resource) DeepCopy() *Resource { 30 | if in == nil { 31 | return nil 32 | } 33 | out := new(Resource) 34 | in.DeepCopyInto(out) 35 | return out 36 | } 37 | -------------------------------------------------------------------------------- /pkg/cliruntime/apply.go: -------------------------------------------------------------------------------- 1 | package cliruntime 2 | 3 | import ( 4 | "cloudnativeapp/clm/pkg/utils" 5 | "fmt" 6 | "k8s.io/apimachinery/pkg/api/errors" 7 | "k8s.io/apimachinery/pkg/api/meta" 8 | "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" 9 | "k8s.io/apimachinery/pkg/runtime" 10 | utilerrors "k8s.io/apimachinery/pkg/util/errors" 11 | "k8s.io/apimachinery/pkg/util/sets" 12 | "k8s.io/cli-runtime/pkg/genericclioptions" 13 | "k8s.io/cli-runtime/pkg/resource" 14 | cmdutil "k8s.io/kubectl/pkg/cmd/util" 15 | "net/url" 16 | ctrl "sigs.k8s.io/controller-runtime" 17 | "strings" 18 | ) 19 | 20 | type ApplyOptions struct { 21 | Builder *resource.Builder 22 | objects []*resource.Info 23 | Selector string 24 | VisitedNamespaces sets.String 25 | VisitedUids sets.String 26 | Urls []string 27 | YamlStr string 28 | IgnoreError bool 29 | } 30 | 31 | const ( 32 | kubectlPrefix = "kubectl.kubernetes.io/" 33 | 34 | LastAppliedConfigAnnotation = kubectlPrefix + "last-applied-configuration" 35 | ) 36 | 37 | var cLog = ctrl.Log.WithName("cli-runtime") 38 | 39 | var metadataAccessor = meta.NewAccessor() 40 | 41 | func NewApplyOptions(urls []string, yamlStr string, ignoreError bool) *ApplyOptions { 42 | n := &ApplyOptions{} 43 | configFlags := genericclioptions.NewConfigFlags(true).WithDeprecatedPasswordFlag() 44 | n.Builder = resource.NewBuilder(configFlags) 45 | n.Urls = urls 46 | n.YamlStr = yamlStr 47 | n.VisitedNamespaces = sets.NewString() 48 | n.VisitedUids = sets.NewString() 49 | n.IgnoreError = ignoreError 50 | return n 51 | } 52 | 53 | func (n *ApplyOptions) GetObjects() ([]*resource.Info, error) { 54 | cLog.V(utils.Debug).Info("try to get objects from stream") 55 | var err error = nil 56 | var result *resource.Result 57 | b := n.Builder. 58 | Unstructured(). 59 | ContinueOnError(). 60 | DefaultNamespace(). 61 | LabelSelectorParam(n.Selector). 62 | Flatten() 63 | if len(n.YamlStr) != 0 { 64 | b.Stream(strings.NewReader(n.YamlStr), "test") 65 | } 66 | 67 | for _, s := range n.Urls { 68 | if strings.Index(s, "http://") == 0 || strings.Index(s, "https://") == 0 { 69 | url, err := url.Parse(s) 70 | if err != nil { 71 | cLog.Error(err, fmt.Sprintf("the URL passed to filename %q is not valid", s)) 72 | return nil, err 73 | } 74 | b.URL(3, url) 75 | } 76 | } 77 | 78 | result = b.Do() 79 | n.objects, err = result.Infos() 80 | 81 | return n.objects, err 82 | } 83 | 84 | func (n *ApplyOptions) Run() error { 85 | cLog.V(utils.Debug).Info("start apply") 86 | errs := []error{} 87 | infos, err := n.GetObjects() 88 | if err != nil { 89 | errs = append(errs, err) 90 | } 91 | if len(infos) == 0 && len(errs) == 0 { 92 | return fmt.Errorf("no objects passed to apply") 93 | } 94 | // Iterate through all objects, applying each one. 95 | for _, info := range infos { 96 | cLog.V(utils.Info).Info(fmt.Sprintf("apply %s", info.Name)) 97 | if err := n.applyOneObject(info); err != nil { 98 | errs = append(errs, err) 99 | } 100 | } 101 | 102 | // If any errors occurred during apply, then return error (or 103 | // aggregate of errors). 104 | if len(errs) == 1 { 105 | return errs[0] 106 | } 107 | if len(errs) > 1 { 108 | return utilerrors.NewAggregate(errs) 109 | } 110 | return nil 111 | } 112 | 113 | func (n *ApplyOptions) MarkNamespaceVisited(info *resource.Info) { 114 | if info.Namespaced() { 115 | n.VisitedNamespaces.Insert(info.Namespace) 116 | } 117 | } 118 | 119 | func (n *ApplyOptions) MarkObjectVisited(info *resource.Info) error { 120 | metadata, err := meta.Accessor(info.Object) 121 | if err != nil { 122 | return err 123 | } 124 | n.VisitedUids.Insert(string(metadata.GetUID())) 125 | return nil 126 | } 127 | 128 | func (n *ApplyOptions) applyOneObject(info *resource.Info) error { 129 | cLog.V(utils.Debug).Info("start apply one object", "info", info.Name) 130 | n.MarkNamespaceVisited(info) 131 | helper := resource.NewHelper(info.Client, info.Mapping) 132 | 133 | modified, err := GetModifiedConfiguration(info.Object, true, unstructured.UnstructuredJSONScheme) 134 | if err != nil { 135 | return cmdutil.AddSourceToErr(fmt.Sprintf("retrieving modified configuration from:\n%s\nfor:", info.String()), info.Source, err) 136 | } 137 | 138 | if err := info.Get(); err != nil { 139 | if !errors.IsNotFound(err) { 140 | return cmdutil.AddSourceToErr(fmt.Sprintf("retrieving current configuration of:\n%s\nfrom server for:", info.String()), info.Source, err) 141 | } 142 | // Create the resource if it doesn't exist 143 | // First, update the annotation used by kubectl apply 144 | if err := CreateApplyAnnotation(info.Object, unstructured.UnstructuredJSONScheme); err != nil { 145 | return cmdutil.AddSourceToErr("creating", info.Source, err) 146 | } 147 | // Then create the resource and skip the three-way merge 148 | obj, err := helper.Create(info.Namespace, true, info.Object) 149 | if err != nil { 150 | return cmdutil.AddSourceToErr("creating", info.Source, err) 151 | } 152 | info.Refresh(obj, n.IgnoreError) 153 | 154 | if err := n.MarkObjectVisited(info); err != nil { 155 | return err 156 | } 157 | //todo wait resource ready 158 | return nil 159 | } 160 | 161 | if err := n.MarkObjectVisited(info); err != nil { 162 | return err 163 | } 164 | 165 | metadata, _ := meta.Accessor(info.Object) 166 | annotationMap := metadata.GetAnnotations() 167 | if _, ok := annotationMap[LastAppliedConfigAnnotation]; !ok { 168 | cLog.V(utils.Warn).Info("annotationMap err") 169 | } 170 | 171 | patcher, err := newPatcher(info, helper) 172 | if err != nil { 173 | return err 174 | } 175 | patchBytes, patchedObject, err := patcher.Patch(info.Object, modified, info.Source, info.Namespace, info.Name, nil) 176 | if err != nil { 177 | return cmdutil.AddSourceToErr(fmt.Sprintf("applying patch:\n%s\nto:\n%v\nfor:", patchBytes, info), info.Source, err) 178 | } 179 | 180 | info.Refresh(patchedObject, true) 181 | return nil 182 | } 183 | 184 | func GetModifiedConfiguration(obj runtime.Object, annotate bool, codec runtime.Encoder) ([]byte, error) { 185 | var modified []byte 186 | annots, err := metadataAccessor.Annotations(obj) 187 | if err != nil { 188 | return nil, err 189 | } 190 | 191 | if annots == nil { 192 | annots = map[string]string{} 193 | } 194 | 195 | original := annots[LastAppliedConfigAnnotation] 196 | delete(annots, LastAppliedConfigAnnotation) 197 | if err := metadataAccessor.SetAnnotations(obj, annots); err != nil { 198 | return nil, err 199 | } 200 | 201 | modified, err = runtime.Encode(codec, obj) 202 | if err != nil { 203 | return nil, err 204 | } 205 | 206 | if annotate { 207 | annots[LastAppliedConfigAnnotation] = string(modified) 208 | if err := metadataAccessor.SetAnnotations(obj, annots); err != nil { 209 | return nil, err 210 | } 211 | 212 | modified, err = runtime.Encode(codec, obj) 213 | if err != nil { 214 | return nil, err 215 | } 216 | } 217 | 218 | annots[LastAppliedConfigAnnotation] = original 219 | if err := metadataAccessor.SetAnnotations(obj, annots); err != nil { 220 | return nil, err 221 | } 222 | 223 | return modified, nil 224 | } 225 | 226 | func CreateApplyAnnotation(obj runtime.Object, codec runtime.Encoder) error { 227 | modified, err := GetModifiedConfiguration(obj, false, codec) 228 | if err != nil { 229 | return err 230 | } 231 | return setOriginalConfiguration(obj, modified) 232 | } 233 | 234 | func setOriginalConfiguration(obj runtime.Object, original []byte) error { 235 | if len(original) < 1 { 236 | return nil 237 | } 238 | 239 | annots, err := metadataAccessor.Annotations(obj) 240 | if err != nil { 241 | return err 242 | } 243 | 244 | if annots == nil { 245 | annots = map[string]string{} 246 | } 247 | 248 | annots[LastAppliedConfigAnnotation] = string(original) 249 | return metadataAccessor.SetAnnotations(obj, annots) 250 | } 251 | -------------------------------------------------------------------------------- /pkg/cliruntime/delete.go: -------------------------------------------------------------------------------- 1 | package cliruntime 2 | 3 | import ( 4 | "cloudnativeapp/clm/pkg/utils" 5 | "fmt" 6 | "github.com/pkg/errors" 7 | "k8s.io/apimachinery/pkg/api/meta" 8 | metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" 9 | "k8s.io/apimachinery/pkg/runtime" 10 | "k8s.io/cli-runtime/pkg/genericclioptions" 11 | "k8s.io/cli-runtime/pkg/resource" 12 | "k8s.io/klog/v2" 13 | cmdutil "k8s.io/kubectl/pkg/cmd/util" 14 | cmdwait "k8s.io/kubectl/pkg/cmd/wait" 15 | "net/url" 16 | "strings" 17 | ) 18 | 19 | const ( 20 | CRD_NOT_EXIST_ERROR = "doesn't have a resource type" 21 | ) 22 | 23 | type DeleteOptions struct { 24 | Builder *resource.Builder 25 | GracePeriod int 26 | Result *resource.Result 27 | } 28 | 29 | func NewDeleteOptions(urlsInput []string, yamlStr string) (*DeleteOptions, error) { 30 | d := &DeleteOptions{} 31 | d.GracePeriod = 1 32 | configFlags := genericclioptions.NewConfigFlags(true).WithDeprecatedPasswordFlag() 33 | d.Builder = resource.NewBuilder(configFlags) 34 | r := d.Builder. 35 | Unstructured(). 36 | ContinueOnError(). 37 | DefaultNamespace(). 38 | SelectAllParam(false). 39 | AllNamespaces(false). 40 | RequireObject(false). 41 | Flatten() 42 | if len(yamlStr) != 0 { 43 | r.Stream(strings.NewReader(yamlStr), "test") 44 | } 45 | 46 | for _, s := range urlsInput { 47 | if strings.Index(s, "http://") == 0 || strings.Index(s, "https://") == 0 { 48 | url, err := url.Parse(s) 49 | if err != nil { 50 | cLog.V(utils.Warn).Info("the URL passed to filename is not valid", 51 | "filename", s, "error", err.Error()) 52 | continue 53 | } 54 | r.URL(3, url) 55 | } 56 | } 57 | 58 | result := r.Do() 59 | err := result.Err() 60 | if err != nil { 61 | return nil, err 62 | } 63 | d.Result = result 64 | return d, nil 65 | } 66 | 67 | func (o *DeleteOptions) RunDelete() error { 68 | cLog.V(utils.Debug).Info("start delete") 69 | return o.DeleteResult(o.Result) 70 | } 71 | 72 | func (o *DeleteOptions) DeleteResult(r *resource.Result) error { 73 | found := 0 74 | uidMap := cmdwait.UIDMap{} 75 | err := r.Visit(func(info *resource.Info, err error) error { 76 | cLog.V(utils.Debug).Info("start delete an info", "info", info.Name) 77 | if err != nil { 78 | return err 79 | } 80 | found++ 81 | 82 | if err := validateCRDStatus(info); err != nil { 83 | return err 84 | } 85 | 86 | options := &metav1.DeleteOptions{} 87 | if o.GracePeriod >= 0 { 88 | options = metav1.NewDeleteOptions(int64(o.GracePeriod)) 89 | } 90 | policy := metav1.DeletePropagationBackground 91 | options.PropagationPolicy = &policy 92 | response, err := o.deleteResource(info, options) 93 | if err != nil { 94 | return err 95 | } 96 | resourceLocation := cmdwait.ResourceLocation{ 97 | GroupResource: info.Mapping.Resource.GroupResource(), 98 | Namespace: info.Namespace, 99 | Name: info.Name, 100 | } 101 | if status, ok := response.(*metav1.Status); ok && status.Details != nil { 102 | uidMap[resourceLocation] = status.Details.UID 103 | return nil 104 | } 105 | responseMetadata, err := meta.Accessor(response) 106 | if err != nil { 107 | // we don't have UID, but we didn't fail the delete, next best thing is just skipping the UID 108 | klog.V(1).Info(err) 109 | return nil 110 | } 111 | uidMap[resourceLocation] = responseMetadata.GetUID() 112 | 113 | return nil 114 | }) 115 | if err != nil { 116 | return err 117 | } 118 | if found == 0 { 119 | cLog.V(utils.Warn).Info("No resources found") 120 | return nil 121 | } 122 | 123 | return nil 124 | } 125 | 126 | func validateCRDStatus(info *resource.Info) error { 127 | gvk := info.Object.GetObjectKind() 128 | if gvk != nil && strings.ToLower(gvk.GroupVersionKind().Kind) == "customresourcedefinition" { 129 | if count, err := CheckCRNum(info.Name); err != nil { 130 | return err 131 | } else if count == 0 { 132 | return nil 133 | } else { 134 | return errors.New(fmt.Sprintf("can not delete CRD %s with CR.", gvk.GroupVersionKind().Group)) 135 | } 136 | } 137 | return nil 138 | } 139 | 140 | func CheckCRNum(kind string) (int, error) { 141 | g := NewGetOption(true, "", true) 142 | count, err := g.Run([]string{kind}) 143 | if err != nil { 144 | if strings.Contains(strings.ToLower(err.Error()), CRD_NOT_EXIST_ERROR) { 145 | return 0, nil 146 | } 147 | cLog.V(utils.Debug).Info("find resource error", "error", err.Error()) 148 | return 0, err 149 | } 150 | return count, nil 151 | } 152 | 153 | func (o *DeleteOptions) deleteResource(info *resource.Info, deleteOptions *metav1.DeleteOptions) (runtime.Object, error) { 154 | deleteResponse, err := resource. 155 | NewHelper(info.Client, info.Mapping). 156 | DeleteWithOptions(info.Namespace, info.Name, deleteOptions) 157 | if err != nil { 158 | return nil, cmdutil.AddSourceToErr("deleting", info.Source, err) 159 | } 160 | 161 | return deleteResponse, nil 162 | } 163 | -------------------------------------------------------------------------------- /pkg/cliruntime/get.go: -------------------------------------------------------------------------------- 1 | package cliruntime 2 | 3 | import ( 4 | "cloudnativeapp/clm/pkg/utils" 5 | apierrors "k8s.io/apimachinery/pkg/api/errors" 6 | "k8s.io/cli-runtime/pkg/genericclioptions" 7 | "k8s.io/cli-runtime/pkg/resource" 8 | ) 9 | 10 | type GetOption struct { 11 | AllNamespaces bool 12 | Namespace string 13 | IgnoreNotFound bool 14 | Builder *resource.Builder 15 | } 16 | 17 | func NewGetOption(ignoreNotFound bool, namespace string, allNamespace bool) *GetOption { 18 | g := &GetOption{} 19 | configFlags := genericclioptions.NewConfigFlags(true).WithDeprecatedPasswordFlag() 20 | g.Builder = resource.NewBuilder(configFlags) 21 | g.Namespace = namespace 22 | g.IgnoreNotFound = ignoreNotFound 23 | g.AllNamespaces = allNamespace 24 | return g 25 | } 26 | 27 | func (g *GetOption) Run(args []string) (int, error) { 28 | cLog.V(utils.Debug).Info("start get") 29 | r := g.Builder. 30 | Unstructured(). 31 | NamespaceParam(g.Namespace).DefaultNamespace().AllNamespaces(g.AllNamespaces). 32 | ResourceTypeOrNameArgs(true, args...). 33 | ContinueOnError(). 34 | Latest(). 35 | Flatten(). 36 | Do() 37 | 38 | if g.IgnoreNotFound { 39 | r.IgnoreErrors(apierrors.IsNotFound) 40 | } 41 | if err := r.Err(); err != nil { 42 | return 0, err 43 | } 44 | 45 | rs, err := r.Infos() 46 | if err != nil { 47 | return 0, err 48 | } 49 | cLog.V(utils.Debug).Info("resource got", "result", rs) 50 | 51 | return len(rs), nil 52 | } 53 | -------------------------------------------------------------------------------- /pkg/cliruntime/patcher.go: -------------------------------------------------------------------------------- 1 | //todo rewrite it 2 | package cliruntime 3 | 4 | import ( 5 | "encoding/json" 6 | "fmt" 7 | "io" 8 | "time" 9 | 10 | "github.com/jonboulle/clockwork" 11 | "k8s.io/apimachinery/pkg/api/errors" 12 | "k8s.io/apimachinery/pkg/api/meta" 13 | metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" 14 | "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" 15 | "k8s.io/apimachinery/pkg/runtime" 16 | "k8s.io/apimachinery/pkg/types" 17 | "k8s.io/apimachinery/pkg/util/jsonmergepatch" 18 | "k8s.io/apimachinery/pkg/util/mergepatch" 19 | "k8s.io/apimachinery/pkg/util/strategicpatch" 20 | "k8s.io/apimachinery/pkg/util/wait" 21 | "k8s.io/cli-runtime/pkg/resource" 22 | oapi "k8s.io/kube-openapi/pkg/util/proto" 23 | cmdutil "k8s.io/kubectl/pkg/cmd/util" 24 | "k8s.io/kubectl/pkg/scheme" 25 | "k8s.io/kubectl/pkg/util" 26 | "k8s.io/kubectl/pkg/util/openapi" 27 | ) 28 | 29 | const ( 30 | // maxPatchRetry is the maximum number of conflicts retry for during a patch operation before returning failure 31 | maxPatchRetry = 5 32 | // backOffPeriod is the period to back off when apply patch results in error. 33 | backOffPeriod = 1 * time.Second 34 | // how many times we can retry before back off 35 | triesBeforeBackOff = 1 36 | ) 37 | 38 | // Patcher defines options to patch OpenAPI objects. 39 | type Patcher struct { 40 | Mapping *meta.RESTMapping 41 | Helper *resource.Helper 42 | 43 | Overwrite bool 44 | BackOff clockwork.Clock 45 | 46 | Force bool 47 | Cascade bool 48 | Timeout time.Duration 49 | GracePeriod int 50 | 51 | // If set, forces the patch against a specific resourceVersion 52 | ResourceVersion *string 53 | 54 | // Number of retries to make if the patch fails with conflict 55 | Retries int 56 | 57 | OpenapiSchema openapi.Resources 58 | } 59 | 60 | func newPatcher(info *resource.Info, helper *resource.Helper) (*Patcher, error) { 61 | var openapiSchema openapi.Resources 62 | 63 | return &Patcher{ 64 | Overwrite: true, 65 | Mapping: info.Mapping, 66 | Helper: helper, 67 | BackOff: clockwork.NewRealClock(), 68 | OpenapiSchema: openapiSchema, 69 | Retries: maxPatchRetry, 70 | }, nil 71 | } 72 | 73 | func (p *Patcher) delete(namespace, name string) error { 74 | options := asDeleteOptions(p.Cascade, p.GracePeriod) 75 | _, err := p.Helper.DeleteWithOptions(namespace, name, &options) 76 | return err 77 | } 78 | 79 | func asDeleteOptions(cascade bool, gracePeriod int) metav1.DeleteOptions { 80 | options := metav1.DeleteOptions{} 81 | if gracePeriod >= 0 { 82 | options = *metav1.NewDeleteOptions(int64(gracePeriod)) 83 | } 84 | policy := metav1.DeletePropagationForeground 85 | if !cascade { 86 | policy = metav1.DeletePropagationOrphan 87 | } 88 | options.PropagationPolicy = &policy 89 | return options 90 | } 91 | 92 | func (p *Patcher) patchSimple(obj runtime.Object, modified []byte, source, namespace, name string, errOut io.Writer) ([]byte, runtime.Object, error) { 93 | // Serialize the current configuration of the object from the server. 94 | current, err := runtime.Encode(unstructured.UnstructuredJSONScheme, obj) 95 | if err != nil { 96 | return nil, nil, cmdutil.AddSourceToErr(fmt.Sprintf("serializing current configuration from:\n%v\nfor:", obj), source, err) 97 | } 98 | 99 | // Retrieve the original configuration of the object from the annotation. 100 | original, err := util.GetOriginalConfiguration(obj) 101 | if err != nil { 102 | return nil, nil, cmdutil.AddSourceToErr(fmt.Sprintf("retrieving original configuration from:\n%v\nfor:", obj), source, err) 103 | } 104 | 105 | var patchType types.PatchType 106 | var patch []byte 107 | var lookupPatchMeta strategicpatch.LookupPatchMeta 108 | var schema oapi.Schema 109 | createPatchErrFormat := "creating patch with:\noriginal:\n%s\nmodified:\n%s\ncurrent:\n%s\nfor:" 110 | 111 | // Create the versioned struct from the type defined in the restmapping 112 | // (which is the API version we'll be submitting the patch to) 113 | versionedObject, err := scheme.Scheme.New(p.Mapping.GroupVersionKind) 114 | switch { 115 | case runtime.IsNotRegisteredError(err): 116 | // fall back to generic JSON merge patch 117 | patchType = types.MergePatchType 118 | preconditions := []mergepatch.PreconditionFunc{mergepatch.RequireKeyUnchanged("apiVersion"), 119 | mergepatch.RequireKeyUnchanged("kind"), mergepatch.RequireMetadataKeyUnchanged("name")} 120 | patch, err = jsonmergepatch.CreateThreeWayJSONMergePatch(original, modified, current, preconditions...) 121 | if err != nil { 122 | if mergepatch.IsPreconditionFailed(err) { 123 | return nil, nil, fmt.Errorf("%s", "At least one of apiVersion, kind and name was changed") 124 | } 125 | return nil, nil, cmdutil.AddSourceToErr(fmt.Sprintf(createPatchErrFormat, original, modified, current), source, err) 126 | } 127 | case err != nil: 128 | return nil, nil, cmdutil.AddSourceToErr(fmt.Sprintf("getting instance of versioned object for %v:", p.Mapping.GroupVersionKind), source, err) 129 | case err == nil: 130 | // Compute a three way strategic merge patch to send to server. 131 | patchType = types.StrategicMergePatchType 132 | 133 | // Try to use openapi first if the openapi spec is available and can successfully calculate the patch. 134 | // Otherwise, fall back to baked-in types. 135 | if p.OpenapiSchema != nil { 136 | if schema = p.OpenapiSchema.LookupResource(p.Mapping.GroupVersionKind); schema != nil { 137 | lookupPatchMeta = strategicpatch.PatchMetaFromOpenAPI{Schema: schema} 138 | if openapiPatch, err := strategicpatch.CreateThreeWayMergePatch(original, modified, current, lookupPatchMeta, p.Overwrite); err != nil { 139 | fmt.Fprintf(errOut, "warning: error calculating patch from openapi spec: %v\n", err) 140 | } else { 141 | patchType = types.StrategicMergePatchType 142 | patch = openapiPatch 143 | } 144 | } 145 | } 146 | 147 | if patch == nil { 148 | lookupPatchMeta, err = strategicpatch.NewPatchMetaFromStruct(versionedObject) 149 | if err != nil { 150 | return nil, nil, cmdutil.AddSourceToErr(fmt.Sprintf(createPatchErrFormat, original, modified, current), source, err) 151 | } 152 | patch, err = strategicpatch.CreateThreeWayMergePatch(original, modified, current, lookupPatchMeta, p.Overwrite) 153 | if err != nil { 154 | return nil, nil, cmdutil.AddSourceToErr(fmt.Sprintf(createPatchErrFormat, original, modified, current), source, err) 155 | } 156 | } 157 | } 158 | 159 | if string(patch) == "{}" { 160 | return patch, obj, nil 161 | } 162 | 163 | if p.ResourceVersion != nil { 164 | patch, err = addResourceVersion(patch, *p.ResourceVersion) 165 | if err != nil { 166 | return nil, nil, cmdutil.AddSourceToErr("Failed to insert resourceVersion in patch", source, err) 167 | } 168 | } 169 | 170 | patchedObj, err := p.Helper.Patch(namespace, name, patchType, patch, nil) 171 | return patch, patchedObj, err 172 | } 173 | 174 | // Patch tries to patch an OpenAPI resource. On success, returns the merge patch as well 175 | // the final patched object. On failure, returns an error. 176 | func (p *Patcher) Patch(current runtime.Object, modified []byte, source, namespace, name string, errOut io.Writer) ([]byte, runtime.Object, error) { 177 | var getErr error 178 | patchBytes, patchObject, err := p.patchSimple(current, modified, source, namespace, name, errOut) 179 | if p.Retries == 0 { 180 | p.Retries = maxPatchRetry 181 | } 182 | for i := 1; i <= p.Retries && errors.IsConflict(err); i++ { 183 | if i > triesBeforeBackOff { 184 | p.BackOff.Sleep(backOffPeriod) 185 | } 186 | current, getErr = p.Helper.Get(namespace, name) 187 | if getErr != nil { 188 | return nil, nil, getErr 189 | } 190 | patchBytes, patchObject, err = p.patchSimple(current, modified, source, namespace, name, errOut) 191 | } 192 | if err != nil && (errors.IsConflict(err) || errors.IsInvalid(err)) && p.Force { 193 | patchBytes, patchObject, err = p.deleteAndCreate(current, modified, namespace, name) 194 | } 195 | return patchBytes, patchObject, err 196 | } 197 | 198 | func (p *Patcher) deleteAndCreate(original runtime.Object, modified []byte, namespace, name string) ([]byte, runtime.Object, error) { 199 | if err := p.delete(namespace, name); err != nil { 200 | return modified, nil, err 201 | } 202 | // TODO: use wait 203 | if err := wait.PollImmediate(1*time.Second, p.Timeout, func() (bool, error) { 204 | if _, err := p.Helper.Get(namespace, name); !errors.IsNotFound(err) { 205 | return false, err 206 | } 207 | return true, nil 208 | }); err != nil { 209 | return modified, nil, err 210 | } 211 | versionedObject, _, err := unstructured.UnstructuredJSONScheme.Decode(modified, nil, nil) 212 | if err != nil { 213 | return modified, nil, err 214 | } 215 | createdObject, err := p.Helper.Create(namespace, true, versionedObject) 216 | if err != nil { 217 | // restore the original object if we fail to create the new one 218 | // but still propagate and advertise error to user 219 | recreated, recreateErr := p.Helper.Create(namespace, true, original) 220 | if recreateErr != nil { 221 | err = fmt.Errorf("An error occurred force-replacing the existing object with the newly provided one:\n\n%v.\n\nAdditionally, an error occurred attempting to restore the original object:\n\n%v", err, recreateErr) 222 | } else { 223 | createdObject = recreated 224 | } 225 | } 226 | return modified, createdObject, err 227 | } 228 | 229 | func addResourceVersion(patch []byte, rv string) ([]byte, error) { 230 | var patchMap map[string]interface{} 231 | err := json.Unmarshal(patch, &patchMap) 232 | if err != nil { 233 | return nil, err 234 | } 235 | u := unstructured.Unstructured{Object: patchMap} 236 | a, err := meta.Accessor(&u) 237 | if err != nil { 238 | return nil, err 239 | } 240 | a.SetResourceVersion(rv) 241 | 242 | return json.Marshal(patchMap) 243 | } 244 | -------------------------------------------------------------------------------- /pkg/dag/dag.go: -------------------------------------------------------------------------------- 1 | package dag 2 | 3 | import ( 4 | "container/list" 5 | ) 6 | 7 | type DAG struct { 8 | graph map[string]*Node 9 | shaped bool 10 | e []string 11 | d []string 12 | } 13 | 14 | func (d *DAG) Init() { 15 | if d.graph == nil { 16 | d.graph = make(map[string]*Node) 17 | } 18 | } 19 | 20 | //AddNode Add one node with dependencies. 21 | func (d *DAG) AddNode(name string, deps []string) bool { 22 | if _, v := d.graph[name]; v { 23 | return false 24 | } 25 | node := NewNode(deps) 26 | d.graph[name] = node 27 | // Need re-shape 28 | d.shaped = false 29 | return true 30 | } 31 | 32 | func (d *DAG) addNodeByNode(name string, node Node) bool { 33 | if _, v := d.graph[name]; v { 34 | return false 35 | } 36 | n := Node{} 37 | n.InCounter = node.InCounter 38 | n.OutCounter = node.OutCounter 39 | n.InEdge = node.InEdge 40 | n.OutEdge = node.OutEdge 41 | d.graph[name] = &n 42 | // Need re-shape 43 | d.shaped = false 44 | return true 45 | } 46 | 47 | //Shape : Check the DAG 48 | func (d *DAG) Shape() bool { 49 | graph := d.graph 50 | for name, node := range graph { 51 | for dep := range node.OutEdge { 52 | if v, ok := graph[dep]; !ok { 53 | return false 54 | } else { 55 | v.InEdge[name] = true 56 | v.InCounter += 1 57 | } 58 | } 59 | } 60 | ok := d.enqueue() && d.dequeue() 61 | if !ok { 62 | return false 63 | } 64 | d.shaped = true 65 | return true 66 | } 67 | 68 | func (d *DAG) queue(enqueue bool) (q []string, success bool) { 69 | graph := d.graph 70 | stack := list.New() 71 | tmpCounter := make(map[string]int) 72 | for name, node := range graph { 73 | if enqueue { 74 | tmpCounter[name] = node.OutCounter 75 | } else { 76 | tmpCounter[name] = node.InCounter 77 | } 78 | if tmpCounter[name] == 0 { 79 | stack.PushBack(name) 80 | } 81 | } 82 | 83 | count := 0 84 | for stack.Len() > 0 { 85 | count++ 86 | item := stack.Front() 87 | stack.Remove(item) 88 | name := item.Value.(string) 89 | q = append(q, name) 90 | node := graph[name] 91 | 92 | var target map[string]bool 93 | if enqueue { 94 | target = node.InEdge 95 | } else { 96 | target = node.OutEdge 97 | } 98 | for iter := range target { 99 | tmpCounter[iter] -= 1 100 | if tmpCounter[iter] == 0 { 101 | stack.PushBack(iter) 102 | } 103 | } 104 | } 105 | 106 | if count != len(graph) { 107 | return q, false 108 | } 109 | return q, true 110 | } 111 | 112 | // 安装出度为0 113 | func (d *DAG) enqueue() bool { 114 | q, ok := d.queue(true) 115 | if ok { 116 | d.e = q 117 | } 118 | return ok 119 | } 120 | 121 | // 卸载入度为0 122 | func (d *DAG) dequeue() bool { 123 | q, ok := d.queue(false) 124 | if ok { 125 | d.d = q 126 | } 127 | return ok 128 | } 129 | 130 | func (d *DAG) isShaped() bool { 131 | return d.shaped 132 | } 133 | 134 | //GetInstallQueue Get the install queue of DAG. 135 | func (d *DAG) GetInstallQueue() (q []string, success bool) { 136 | if !d.isShaped() && !d.Shape() { 137 | success = false 138 | return 139 | } 140 | 141 | return d.e, true 142 | } 143 | 144 | //GetUninstallQueue Get the uninstall queue of DAG. 145 | func (d *DAG) GetUninstallQueue() (q []string, success bool) { 146 | if !d.isShaped() && !d.Shape() { 147 | success = false 148 | return 149 | } 150 | 151 | return d.d, true 152 | } 153 | 154 | //GetUninstallQueueOfNode Get uninstall queue when uninstall one node. 155 | func (d *DAG) GetUninstallQueueOfNode(name string) (q []string, success bool) { 156 | graph := d.graph 157 | stack := list.New() 158 | if _, ok := graph[name]; !ok { 159 | return nil, false 160 | } else { 161 | stack.PushBack(name) 162 | } 163 | 164 | tmp := new(DAG) 165 | tmp.Init() 166 | 167 | for stack.Len() > 0 { 168 | item := stack.Front() 169 | stack.Remove(item) 170 | name := item.Value.(string) 171 | node := graph[name] 172 | tmp.addNodeByNode(name, *node) 173 | 174 | for n := range node.InEdge { 175 | if _, ok := graph[n]; !ok { 176 | success = false 177 | return 178 | } else { 179 | stack.PushBack(n) 180 | } 181 | } 182 | } 183 | 184 | return tmp.queue(false) 185 | } 186 | -------------------------------------------------------------------------------- /pkg/dag/dag_test.go: -------------------------------------------------------------------------------- 1 | package dag 2 | 3 | import ( 4 | "reflect" 5 | "testing" 6 | ) 7 | 8 | func initTest() DAG { 9 | dag := new(DAG) 10 | dag.Init() 11 | dag.AddNode("a", []string{"b"}) 12 | dag.AddNode("b", []string{"d", "c"}) 13 | dag.AddNode("c", []string{}) 14 | dag.AddNode("d", []string{"c"}) 15 | return *dag 16 | } 17 | 18 | func initTestErr() DAG { 19 | dag := new(DAG) 20 | dag.Init() 21 | dag.AddNode("a", []string{"b"}) 22 | dag.AddNode("b", []string{"d", "c"}) 23 | dag.AddNode("c", []string{"a"}) 24 | dag.AddNode("d", []string{"c"}) 25 | return *dag 26 | } 27 | 28 | func TestDAG_Shape_Err(t *testing.T) { 29 | dag := initTestErr() 30 | if ok := dag.Shape(); ok { 31 | t.Errorf("test dag shape failed") 32 | } 33 | } 34 | 35 | func TestDAG_Shape(t *testing.T) { 36 | dag := initTest() 37 | if ok := dag.Shape(); !ok { 38 | t.Errorf("test dag shape failed") 39 | } 40 | } 41 | 42 | func TestDAG_GetInstallQueue(t *testing.T) { 43 | dag := initTest() 44 | dag.Shape() 45 | if q, ok := dag.GetInstallQueue(); !ok { 46 | t.Errorf("get install queue failed") 47 | } else { 48 | t.Log(q) 49 | if !reflect.DeepEqual(q, []string{"c", "d", "b", "a"}) { 50 | t.Errorf("get wrong install queue") 51 | } 52 | } 53 | } 54 | 55 | func TestDAG_GetUninstallQueue(t *testing.T) { 56 | dag := initTest() 57 | dag.Shape() 58 | if q, ok := dag.GetUninstallQueue(); !ok { 59 | t.Errorf("get uninstall queue failed") 60 | } else { 61 | t.Log(q) 62 | if !reflect.DeepEqual(q, []string{"a", "b", "d", "c"}) { 63 | t.Errorf("get wrong uninstall queue") 64 | } 65 | } 66 | 67 | } 68 | 69 | func TestDAG_GetUninstallQueueOfNode(t *testing.T) { 70 | dag := initTest() 71 | dag.Shape() 72 | if q, ok := dag.GetUninstallQueueOfNode("d"); !ok { 73 | t.Errorf("get uninstall queue failed") 74 | } else { 75 | t.Log(q) 76 | if !reflect.DeepEqual(q, []string{"a", "b", "d"}) { 77 | t.Errorf("get wrong uninstall queue of node %s", "d") 78 | } 79 | } 80 | } 81 | -------------------------------------------------------------------------------- /pkg/dag/node.go: -------------------------------------------------------------------------------- 1 | package dag 2 | 3 | type Node struct { 4 | OutEdge map[string]bool 5 | InEdge map[string]bool 6 | OutCounter int 7 | InCounter int 8 | } 9 | 10 | func NewNode(deps []string) *Node { 11 | taskNode := new(Node) 12 | taskNode.OutCounter = 0 13 | taskNode.InCounter = 0 14 | taskNode.OutEdge = make(map[string]bool) 15 | taskNode.InEdge = make(map[string]bool) 16 | for _, dep := range deps { 17 | taskNode.OutEdge[dep] = true 18 | taskNode.OutCounter += 1 19 | } 20 | return taskNode 21 | } 22 | -------------------------------------------------------------------------------- /pkg/download/http.go: -------------------------------------------------------------------------------- 1 | package download 2 | 3 | import ( 4 | "errors" 5 | "fmt" 6 | "io" 7 | "net/http" 8 | "net/url" 9 | "os" 10 | "path/filepath" 11 | "time" 12 | ) 13 | 14 | func HttpGet(p string) (string, error) { 15 | u, err := url.Parse(p) 16 | if err != nil || u.Scheme == "" { 17 | return "", nil 18 | } else { 19 | return HttpAndFileSchemeDownload(p, u) 20 | } 21 | 22 | } 23 | 24 | func HttpAndFileSchemeDownload(p string, url *url.URL) (string, error) { 25 | tr := &http.Transport{} 26 | tr.RegisterProtocol("file", http.NewFileTransport(http.Dir("/"))) 27 | c := &http.Client{Transport: tr} 28 | c.Timeout = 3600 * time.Second 29 | resp, err := c.Get(p) 30 | if err != nil { 31 | return "", err 32 | } 33 | defer resp.Body.Close() 34 | 35 | if resp.StatusCode == http.StatusOK { 36 | _, f := filepath.Split(url.Path) 37 | 38 | path := filepath.Join(os.TempDir(), f) 39 | out, err := os.Create(path) 40 | if err != nil { 41 | return "", err 42 | } 43 | defer out.Close() 44 | _, err = io.Copy(out, resp.Body) 45 | if err != nil { 46 | return "", err 47 | } 48 | return path, nil 49 | 50 | } else { 51 | return "", errors.New(fmt.Sprintf("download failed, http response:%s.", resp.Status)) 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /pkg/helmsdk/helm.go: -------------------------------------------------------------------------------- 1 | package helmsdk 2 | 3 | import ( 4 | "cloudnativeapp/clm/pkg/download" 5 | "cloudnativeapp/clm/pkg/utils" 6 | "fmt" 7 | "helm.sh/helm/v3/pkg/action" 8 | "helm.sh/helm/v3/pkg/chart/loader" 9 | "helm.sh/helm/v3/pkg/cli" 10 | "helm.sh/helm/v3/pkg/release" 11 | "os" 12 | ctrl "sigs.k8s.io/controller-runtime" 13 | "time" 14 | ) 15 | 16 | var hLog = ctrl.Log.WithName("helm-sdk") 17 | 18 | func Install(chartPath, releaseName, namespace string, vals map[string]interface{}, wait bool, timeouts time.Duration) (string, error) { 19 | hLog.V(utils.Debug).Info("try to install", "chartPath", chartPath, "releaseName", releaseName, 20 | "namespace", namespace, "values", vals, "wait", wait, "timeouts", timeouts) 21 | actionConfig, err := getActionConfig(namespace) 22 | if err != nil { 23 | return "", err 24 | } 25 | client := action.NewInstall(actionConfig) 26 | client.Namespace = namespace 27 | client.ReleaseName = releaseName 28 | if wait { 29 | client.Wait = true 30 | client.Timeout = timeouts 31 | } 32 | charts, err := loader.Load(chartPath) 33 | if err != nil { 34 | return "", err 35 | } 36 | hLog.V(utils.Debug).Info("charts load", "charts", charts) 37 | results, err := client.Run(charts, vals) 38 | if err != nil { 39 | return "", err 40 | } 41 | hLog.V(utils.Debug).Info("charts installed", "result", results) 42 | return results.Name, nil 43 | } 44 | 45 | func Uninstall(chartPath, releaseName, namespace string) (string, error) { 46 | hLog.V(utils.Debug).Info("try to uninstall", "chartPath", chartPath, 47 | "releaseName", releaseName, "namespace", namespace) 48 | chartPathLocal, err := download.HttpGet(chartPath) 49 | if err != nil { 50 | return "", err 51 | } 52 | if len(chartPathLocal) == 0 { 53 | chartPathLocal, err = LocateChart(chartPath, namespace) 54 | if err != nil { 55 | return "", err 56 | } 57 | } 58 | if exist, err := Exist(chartPathLocal, namespace); err != nil { 59 | return "", err 60 | } else if !exist { 61 | hLog.V(utils.Info).Info("no chart found", "chartPath", chartPath, 62 | "releaseName", releaseName, "namespace", namespace) 63 | return "", nil 64 | } 65 | actionConfig, err := getActionConfig(namespace) 66 | if err != nil { 67 | return "", err 68 | } 69 | c := action.NewUninstall(actionConfig) 70 | r, err := c.Run(releaseName) 71 | if err != nil { 72 | return "", err 73 | } 74 | hLog.V(utils.Debug).Info("charts uninstalled", "result", r) 75 | return r.Release.Name, nil 76 | } 77 | 78 | func Status(releaseName, namespace string) (*release.Release, error) { 79 | hLog.V(utils.Debug).Info("try to status", "releaseName", releaseName, "namespace", namespace) 80 | actionConfig, err := getActionConfig(namespace) 81 | if err != nil { 82 | return nil, err 83 | } 84 | 85 | client := action.NewStatus(actionConfig) 86 | r, err := client.Run(releaseName) 87 | if err != nil { 88 | return nil, err 89 | } 90 | return r, nil 91 | } 92 | 93 | func List(namespace string) ([]*release.Release, error) { 94 | hLog.V(utils.Debug).Info("try to list", "namespace", namespace) 95 | actionConfig, err := getActionConfig(namespace) 96 | if err != nil { 97 | return nil, err 98 | } 99 | client := action.NewList(actionConfig) 100 | r, err := client.Run() 101 | if err != nil { 102 | return nil, err 103 | } 104 | return r, nil 105 | } 106 | 107 | func Upgrade(chartPath, releaseName, namespace string, vals map[string]interface{}, wait bool, timeouts time.Duration) (string, error) { 108 | hLog.V(utils.Debug).Info("try to upgrade", "chartPath", chartPath, "releaseName", releaseName, 109 | "namespace", namespace, "values", vals, "wait", wait, "timeouts", timeouts) 110 | actionConfig, err := getActionConfig(namespace) 111 | if err != nil { 112 | return "", err 113 | } 114 | charts, err := loader.Load(chartPath) 115 | if err != nil { 116 | return "", err 117 | } 118 | hLog.V(utils.Debug).Info("charts load", "charts", charts) 119 | client := action.NewUpgrade(actionConfig) 120 | client.ReuseValues = true 121 | if wait { 122 | client.Wait = true 123 | client.Timeout = timeouts 124 | } 125 | r, err := client.Run(releaseName, charts, vals) 126 | if err != nil { 127 | return "", err 128 | } 129 | hLog.V(utils.Debug).Info("charts upgrade", "result", r) 130 | return r.Name, nil 131 | } 132 | 133 | func Exist(chartPath, namespace string) (bool, error) { 134 | target, err := loader.Load(chartPath) 135 | if err != nil { 136 | return false, err 137 | } 138 | hLog.V(utils.Debug).Info("charts load", "charts", target) 139 | charts, err := List(namespace) 140 | if err != nil { 141 | return false, err 142 | } 143 | for _, chart := range charts { 144 | if chart.Chart.Metadata.Name == target.Metadata.Name && 145 | chart.Chart.Metadata.Version == target.Metadata.Version { 146 | return true, nil 147 | } 148 | } 149 | 150 | return false, nil 151 | } 152 | 153 | func getSdkLog() func(format string, v ...interface{}) { 154 | return func(format string, v ...interface{}) { 155 | hLog.V(utils.Debug).Info(fmt.Sprintf(format, v...)) 156 | } 157 | } 158 | 159 | func getActionConfig(namespace string) (*action.Configuration, error) { 160 | settings := cli.New() 161 | settings.EnvVars() 162 | 163 | actionConfig := new(action.Configuration) 164 | if err := actionConfig.Init(settings.RESTClientGetter(), namespace, os.Getenv("HELM_DRIVER"), getSdkLog()); err != nil { 165 | return nil, err 166 | } 167 | return actionConfig, nil 168 | } 169 | 170 | // Locate charts and download 171 | func LocateChart(chart, namespace string) (string, error) { 172 | ch := action.ChartPathOptions{} 173 | os.Setenv("HELM_NAMESPACE", namespace) 174 | cp, err := ch.LocateChart(chart, cli.New()) 175 | if err != nil { 176 | return "", err 177 | } 178 | return cp, nil 179 | } 180 | -------------------------------------------------------------------------------- /pkg/helmsdk/repo.go: -------------------------------------------------------------------------------- 1 | package helmsdk 2 | 3 | import ( 4 | "cloudnativeapp/clm/pkg/utils" 5 | "context" 6 | "github.com/go-logr/logr" 7 | "github.com/gofrs/flock" 8 | "github.com/pkg/errors" 9 | "helm.sh/helm/v3/pkg/cli" 10 | "helm.sh/helm/v3/pkg/getter" 11 | "helm.sh/helm/v3/pkg/repo" 12 | "io/ioutil" 13 | "os" 14 | "path/filepath" 15 | "sigs.k8s.io/yaml" 16 | "strings" 17 | "time" 18 | ) 19 | 20 | type repoAddOptions struct { 21 | name string 22 | url string 23 | username string 24 | password string 25 | forceUpdate bool 26 | allowDeprecatedRepos bool 27 | 28 | certFile string 29 | keyFile string 30 | caFile string 31 | insecureSkipTLSverify bool 32 | 33 | repoFile string 34 | repoCache string 35 | 36 | // Deprecated, but cannot be removed until Helm 4 37 | deprecatedNoUpdate bool 38 | } 39 | 40 | func Add(name, url, username, password string, log logr.Logger) error { 41 | config := cli.New() 42 | o := &repoAddOptions{ 43 | name: name, 44 | url: url, 45 | username: username, 46 | password: password, 47 | repoFile: config.RepositoryConfig, 48 | repoCache: config.RepositoryCache, 49 | } 50 | return o.run(log) 51 | } 52 | 53 | func (o *repoAddOptions) run(log logr.Logger) error { 54 | // Ensure the file directory exists as it is required for file locking 55 | err := os.MkdirAll(filepath.Dir(o.repoFile), os.ModePerm) 56 | if err != nil && !os.IsExist(err) { 57 | return err 58 | } 59 | 60 | // Acquire a file lock for process synchronization 61 | fileLock := flock.New(strings.Replace(o.repoFile, filepath.Ext(o.repoFile), ".lock", 1)) 62 | lockCtx, cancel := context.WithTimeout(context.Background(), 30*time.Second) 63 | defer cancel() 64 | locked, err := fileLock.TryLockContext(lockCtx, time.Second) 65 | if err == nil && locked { 66 | defer fileLock.Unlock() 67 | } 68 | if err != nil { 69 | return err 70 | } 71 | 72 | b, err := ioutil.ReadFile(o.repoFile) 73 | if err != nil && !os.IsNotExist(err) { 74 | return err 75 | } 76 | 77 | var f repo.File 78 | if err := yaml.Unmarshal(b, &f); err != nil { 79 | return err 80 | } 81 | 82 | c := repo.Entry{ 83 | Name: o.name, 84 | URL: o.url, 85 | Username: o.username, 86 | Password: o.password, 87 | CertFile: o.certFile, 88 | KeyFile: o.keyFile, 89 | CAFile: o.caFile, 90 | InsecureSkipTLSverify: o.insecureSkipTLSverify, 91 | } 92 | 93 | if f.Has(o.name) { 94 | existing := f.Get(o.name) 95 | if c != *existing { 96 | 97 | // The input coming in for the name is different from what is already 98 | // configured. Return an error. 99 | return errors.Errorf("repository name (%s) already exists, please specify a different name", o.name) 100 | } 101 | 102 | // The add is idempotent so do nothing 103 | log.V(utils.Info).Info(o.name + "already exists with the same configuration, skipping") 104 | return nil 105 | } 106 | 107 | r, err := repo.NewChartRepository(&c, getter.All(cli.New())) 108 | if err != nil { 109 | return err 110 | } 111 | 112 | if o.repoCache != "" { 113 | r.CachePath = o.repoCache 114 | } 115 | if _, err := r.DownloadIndexFile(); err != nil { 116 | return errors.Wrapf(err, "looks like %q is not a valid chart repository or cannot be reached", o.url) 117 | } 118 | 119 | f.Update(&c) 120 | 121 | if err := f.WriteFile(o.repoFile, 0644); err != nil { 122 | return err 123 | } 124 | log.V(utils.Info).Info(o.name + "has been added to repositories") 125 | return nil 126 | } 127 | -------------------------------------------------------------------------------- /pkg/implement/helm/helm.go: -------------------------------------------------------------------------------- 1 | package helm 2 | 3 | import ( 4 | "cloudnativeapp/clm/pkg/download" 5 | "cloudnativeapp/clm/pkg/helmsdk" 6 | "github.com/pkg/errors" 7 | "time" 8 | ) 9 | 10 | type Implement struct { 11 | Wait bool `json:"wait,omitempty"` 12 | Timeout int `json:"timeout,omitempty"` 13 | IgnoreError bool `json:"ignoreError,omitempty"` 14 | Repositories []Repo `json:"repositories,omitempty"` 15 | } 16 | 17 | type Repo struct { 18 | Name string `json:"name"` 19 | Url string `json:"url"` 20 | UserName string `json:"username,omitempty"` 21 | PassWord string `json:"password,omitempty"` 22 | } 23 | 24 | func Install(i Implement, values map[string]interface{}) (string, error) { 25 | return doInstallOrUpgrade(i, values) 26 | } 27 | 28 | func Upgrade(i Implement, values map[string]interface{}) (string, error) { 29 | return doInstallOrUpgrade(i, values) 30 | } 31 | 32 | func Uninstall(i Implement, values map[string]interface{}) (string, error) { 33 | releaseName, ok := values["releaseName"].(string) 34 | if !ok && len(releaseName) == 0 { 35 | return "", errors.New("release name needed") 36 | } 37 | chartPath, ok := values["chartPath"].(string) 38 | if !ok { 39 | return "", errors.New("chart path needed") 40 | } 41 | namespace, ok := values["namespace"].(string) 42 | if !ok && len(namespace) == 0 { 43 | namespace = "default" 44 | } 45 | result, err := helmsdk.Uninstall(chartPath, releaseName, namespace) 46 | if err != nil && !i.IgnoreError { 47 | return result, err 48 | } 49 | return result, nil 50 | } 51 | 52 | func Recover(i Implement, values map[string]interface{}) (string, error) { 53 | return doInstallOrUpgrade(i, values) 54 | } 55 | 56 | func Status(i Implement, values map[string]interface{}) (string, error) { 57 | releaseName, ok := values["releaseName"].(string) 58 | if !ok && len(releaseName) == 0 { 59 | return "", errors.New("release name needed") 60 | } 61 | namespace, ok := values["namespace"].(string) 62 | if !ok && len(namespace) == 0 { 63 | namespace = "default" 64 | } 65 | s, err := helmsdk.Status(releaseName, namespace) 66 | if err != nil { 67 | return "", err 68 | } 69 | return string(s.Info.Status), nil 70 | } 71 | 72 | func doInstallOrUpgrade(i Implement, values map[string]interface{}) (string, error) { 73 | releaseName, ok := values["releaseName"].(string) 74 | if !ok && len(releaseName) == 0 { 75 | return "", errors.New("release name needed") 76 | } 77 | chartPath, ok := values["chartPath"].(string) 78 | if !ok { 79 | return "", errors.New("chart path needed") 80 | } 81 | namespace, ok := values["namespace"].(string) 82 | if !ok && len(namespace) == 0 { 83 | namespace = "default" 84 | } 85 | var vals map[string]interface{} 86 | if values["chartValues"] != nil { 87 | v, err := decodeValues(values["chartValues"]) 88 | if err != nil { 89 | return "", err 90 | } 91 | vals = v 92 | } 93 | 94 | chartPathLocal, err := download.HttpGet(chartPath) 95 | if err != nil { 96 | return "", err 97 | } 98 | if len(chartPathLocal) == 0 { 99 | chartPathLocal, err = helmsdk.LocateChart(chartPath, namespace) 100 | if err != nil { 101 | return "", err 102 | } 103 | } 104 | 105 | installed, err := helmsdk.Exist(chartPathLocal, namespace) 106 | if err != nil { 107 | return "", err 108 | } 109 | var timeoutSecond time.Duration 110 | if i.Timeout <= 0 { 111 | timeoutSecond = 60 * time.Second 112 | } else { 113 | timeoutSecond = time.Duration(i.Timeout) * time.Second 114 | } 115 | if installed { 116 | return helmsdk.Upgrade(chartPathLocal, releaseName, namespace, vals, i.Wait, timeoutSecond) 117 | } else { 118 | return helmsdk.Install(chartPathLocal, releaseName, namespace, vals, i.Wait, timeoutSecond) 119 | } 120 | } 121 | 122 | func decodeValues(values interface{}) (map[string]interface{}, error) { 123 | var result map[string]interface{} 124 | result, ok := values.(map[string]interface{}) 125 | if !ok { 126 | return nil, errors.New("chartValues format error") 127 | } 128 | return result, nil 129 | } 130 | -------------------------------------------------------------------------------- /pkg/implement/implement.go: -------------------------------------------------------------------------------- 1 | package implement 2 | 3 | import ( 4 | "cloudnativeapp/clm/pkg/implement/helm" 5 | "cloudnativeapp/clm/pkg/implement/native" 6 | "cloudnativeapp/clm/pkg/implement/service" 7 | "cloudnativeapp/clm/pkg/utils" 8 | "errors" 9 | "fmt" 10 | "github.com/go-logr/logr" 11 | ctrl "sigs.k8s.io/controller-runtime" 12 | ) 13 | 14 | type Implement struct { 15 | LocalService *service.Implement `json:"localService,omitempty"` 16 | 17 | Helm *helm.Implement `json:"helm,omitempty"` 18 | // 本地安装 19 | Native *native.Implement `json:"native,omitempty"` 20 | } 21 | 22 | var iLog = ctrl.Log.WithName("implement") 23 | 24 | var serviceFuncMap = make(map[string]func(logr.Logger, service.Implement, map[string]string) (string, error)) 25 | var helmFuncMap = make(map[string]func(helm.Implement, map[string]interface{}) (string, error)) 26 | var nativeFuncMap = make(map[string]func(logr.Logger, native.Implement, map[string]interface{}) (string, error)) 27 | 28 | const ( 29 | Install = "install" 30 | Uninstall = "uninstall" 31 | Recover = "recover" 32 | Upgrade = "upgrade" 33 | ) 34 | 35 | func init() { 36 | serviceFuncMap[Install] = service.Install 37 | serviceFuncMap[Uninstall] = service.Uninstall 38 | serviceFuncMap[Recover] = service.Recover 39 | serviceFuncMap[Upgrade] = service.Upgrade 40 | 41 | helmFuncMap[Install] = helm.Install 42 | helmFuncMap[Uninstall] = helm.Uninstall 43 | helmFuncMap[Recover] = helm.Recover 44 | helmFuncMap[Upgrade] = helm.Upgrade 45 | 46 | nativeFuncMap[Install] = native.Install 47 | nativeFuncMap[Uninstall] = native.Uninstall 48 | nativeFuncMap[Recover] = native.Recover 49 | nativeFuncMap[Upgrade] = native.Upgrade 50 | } 51 | 52 | func (i *Implement) do(action, name, version string, values map[string]interface{}) error { 53 | iLog.V(utils.Debug).Info("try to do implement", "action", action, "name", name, "values", values) 54 | if i.LocalService != nil { 55 | param := service.GetValuesMap(values, name, version) 56 | if s, err := serviceFuncMap[action](iLog, *i.LocalService, 57 | param); err != nil { 58 | iLog.Error(err, fmt.Sprintf("%s implement by service failed", action)) 59 | return err 60 | } else { 61 | iLog.V(utils.Info).Info(fmt.Sprintf("service %s implement success", action), "rsp", s) 62 | return nil 63 | } 64 | } 65 | if i.Helm != nil { 66 | if s, err := helmFuncMap[action](*i.Helm, values); err != nil { 67 | iLog.Error(err, fmt.Sprintf("%s implement by helm failed", action)) 68 | return err 69 | } else { 70 | iLog.V(utils.Info).Info(fmt.Sprintf("helm %s implement success", action), "rsp", s) 71 | return nil 72 | } 73 | } 74 | if i.Native != nil { 75 | if s, err := nativeFuncMap[action](iLog, *i.Native, values); err != nil { 76 | iLog.Error(err, fmt.Sprintf("%s implement by native failed", action)) 77 | return err 78 | } else { 79 | iLog.V(utils.Info).Info(fmt.Sprintf("native %s implement success", action), "rsp", s) 80 | return nil 81 | } 82 | } 83 | return errors.New(utils.ImplementNotFound) 84 | } 85 | 86 | func (i *Implement) Install(name, version string, values map[string]interface{}) error { 87 | return i.do(Install, name, version, values) 88 | } 89 | 90 | func (i *Implement) Uninstall(name, version string, values map[string]interface{}) error { 91 | return i.do(Uninstall, name, version, values) 92 | } 93 | 94 | func (i *Implement) Recover(name, version string, values map[string]interface{}) error { 95 | return i.do(Recover, name, version, values) 96 | } 97 | 98 | func (i *Implement) Upgrade(name, version string, values map[string]interface{}) error { 99 | return i.do(Upgrade, name, version, values) 100 | } 101 | -------------------------------------------------------------------------------- /pkg/implement/native/native.go: -------------------------------------------------------------------------------- 1 | package native 2 | 3 | import ( 4 | "cloudnativeapp/clm/pkg/cliruntime" 5 | "github.com/go-logr/logr" 6 | "strings" 7 | ) 8 | 9 | type Implement struct { 10 | IgnoreError bool `json:"ignoreError,omitempty"` 11 | } 12 | 13 | func Install(log logr.Logger, i Implement, values map[string]interface{}) (string, error) { 14 | return applyAction(log, i, values) 15 | } 16 | 17 | func Uninstall(log logr.Logger, i Implement, values map[string]interface{}) (string, error) { 18 | return deleteAction(log, i, values) 19 | } 20 | 21 | func Upgrade(log logr.Logger, i Implement, values map[string]interface{}) (string, error) { 22 | return applyAction(log, i, values) 23 | } 24 | 25 | func Recover(log logr.Logger, i Implement, values map[string]interface{}) (string, error) { 26 | return applyAction(log, i, values) 27 | } 28 | 29 | func Status(log logr.Logger, i Implement, values map[string]interface{}) (string, error) { 30 | return "", nil 31 | } 32 | 33 | func applyAction(log logr.Logger, i Implement, values map[string]interface{}) (string, error) { 34 | urls, yamls := getUrlAndStream(values) 35 | n := cliruntime.NewApplyOptions(urls, yamls, i.IgnoreError) 36 | err := n.Run() 37 | if err != nil { 38 | log.Error(err, "native apply error") 39 | return "", err 40 | } 41 | return "native apply success", nil 42 | } 43 | 44 | func deleteAction(log logr.Logger, i Implement, values map[string]interface{}) (string, error) { 45 | urls, yamls := getUrlAndStream(values) 46 | d, err := cliruntime.NewDeleteOptions(urls, yamls) 47 | if err != nil { 48 | log.Error(err, "new native delete command failed") 49 | return "", err 50 | } 51 | 52 | err = d.RunDelete() 53 | if err != nil { 54 | log.Error(err, err.Error()) 55 | if strings.Contains(strings.ToLower(err.Error()), "not found") { 56 | return err.Error(), nil 57 | } 58 | if i.IgnoreError { 59 | return err.Error(), nil 60 | } 61 | return "", err 62 | } 63 | 64 | return "native delete success", nil 65 | } 66 | 67 | func getUrlAndStream(values map[string]interface{}) ([]string, string) { 68 | var urls []string 69 | var yamlStr string 70 | if v, ok := values["urls"]; ok { 71 | if v != nil { 72 | for _, i := range v.([]interface{}) { 73 | urls = append(urls, i.(string)) 74 | } 75 | } 76 | } 77 | if v, ok := values["yaml"]; ok { 78 | yamlStr = v.(string) 79 | } 80 | 81 | return urls, yamlStr 82 | } 83 | -------------------------------------------------------------------------------- /pkg/implement/service/service.go: -------------------------------------------------------------------------------- 1 | package service 2 | 3 | import ( 4 | "context" 5 | "encoding/json" 6 | "fmt" 7 | "github.com/go-logr/logr" 8 | "github.com/pkg/errors" 9 | "io/ioutil" 10 | v1 "k8s.io/apimachinery/pkg/apis/meta/v1" 11 | "k8s.io/client-go/kubernetes" 12 | "net/http" 13 | "net/url" 14 | ctrl "sigs.k8s.io/controller-runtime" 15 | "strings" 16 | ) 17 | 18 | type Implement struct { 19 | Name string `json:"name"` 20 | Namespace string `json:"namespace,omitempty"` 21 | Install Spec `json:"install"` 22 | Uninstall Spec `json:"uninstall"` 23 | Upgrade Spec `json:"upgrade"` 24 | Recover Spec `json:"recover"` 25 | Status Spec `json:"status"` 26 | } 27 | 28 | type Spec struct { 29 | // Support Http 30 | Protocol string `json:"protocol,omitempty"` 31 | RelativePath string `json:"relativePath,omitempty"` 32 | Values url.Values `json:"values,omitempty"` 33 | // Supports post(default) put get delete patch 34 | Method string `json:"method,omitempty"` 35 | // Ignore the error of delete response 36 | IgnoreError bool `json:"ignoreError,omitempty"` 37 | } 38 | 39 | type ImplementRsp struct { 40 | Code int `json:"code"` 41 | Msg string `json:"msg"` 42 | Success bool `json:"success"` 43 | } 44 | 45 | func GetValuesMap(input map[string]interface{}, name, version string) map[string]string { 46 | result := make(map[string]string) 47 | result["name"] = name 48 | result["version"] = version 49 | if input != nil { 50 | for k, v := range input { 51 | result[k] = v.(string) 52 | } 53 | } 54 | return result 55 | } 56 | 57 | func Install(log logr.Logger, svc Implement, params map[string]string) (string, error) { 58 | rsp, err := doAction(log, svc.Name, svc.Namespace, params, svc.Install) 59 | if err != nil { 60 | return "", err 61 | } 62 | if rsp.Code != 200 { 63 | err = errors.New("install failed:" + rsp.Msg) 64 | log.Error(err, " message: "+rsp.Msg) 65 | return "", err 66 | } 67 | return rsp.Msg, nil 68 | } 69 | 70 | func Uninstall(log logr.Logger, svc Implement, params map[string]string) (string, error) { 71 | rsp, err := doAction(log, svc.Name, svc.Namespace, params, svc.Uninstall) 72 | if err != nil { 73 | log.Error(err, "uninstall failed") 74 | if svc.Uninstall.IgnoreError { 75 | return "", nil 76 | } 77 | return "", err 78 | } 79 | if rsp.Code != 200 { 80 | err = errors.New("uninstall failed:" + rsp.Msg) 81 | log.Error(err, " message: "+rsp.Msg) 82 | if svc.Uninstall.IgnoreError { 83 | return "", nil 84 | } 85 | return "", err 86 | } 87 | return rsp.Msg, nil 88 | } 89 | 90 | func Upgrade(log logr.Logger, svc Implement, params map[string]string) (string, error) { 91 | rsp, err := doAction(log, svc.Name, svc.Namespace, params, svc.Upgrade) 92 | if err != nil { 93 | return "", err 94 | } 95 | if rsp.Code != 200 { 96 | err = errors.New("upgrade failed:" + rsp.Msg) 97 | log.Error(err, " message: "+rsp.Msg) 98 | return "", err 99 | } 100 | return rsp.Msg, nil 101 | } 102 | 103 | func Recover(log logr.Logger, svc Implement, params map[string]string) (string, error) { 104 | rsp, err := doAction(log, svc.Name, svc.Namespace, params, svc.Recover) 105 | if err != nil { 106 | return "", err 107 | } 108 | if rsp.Code != 200 { 109 | err = errors.New("recover failed:" + rsp.Msg) 110 | log.Error(err, " message: "+rsp.Msg) 111 | return "", err 112 | } 113 | return rsp.Msg, nil 114 | } 115 | 116 | func Status(log logr.Logger, svc Implement, params map[string]string) (string, error) { 117 | rsp, err := doAction(log, svc.Name, svc.Namespace, params, svc.Status) 118 | if err != nil { 119 | return "", err 120 | } 121 | if rsp.Code != 200 { 122 | err = errors.New("status failed:" + rsp.Msg) 123 | log.Error(err, " message: "+rsp.Msg) 124 | return "", err 125 | } 126 | return rsp.Msg, nil 127 | } 128 | 129 | func getClientSet() (*kubernetes.Clientset, error) { 130 | return kubernetes.NewForConfig(ctrl.GetConfigOrDie()) 131 | } 132 | 133 | func getService(name, namespace string) (ip string, port int32, err error) { 134 | k8sClient, err := getClientSet() 135 | if err != nil { 136 | return "", 0, err 137 | } 138 | service, err := k8sClient.CoreV1().Services(namespace).Get(context.Background(), name, v1.GetOptions{}) 139 | if err != nil { 140 | return "", 0, err 141 | } 142 | 143 | // 目前支持tcp和http 144 | for _, port := range service.Spec.Ports { 145 | if port.Protocol == "TCP" || port.Protocol == "HTTP" { 146 | return service.Spec.ClusterIP, port.Port, nil 147 | } 148 | } 149 | 150 | return "", 0, errors.New("no tcp or http port found") 151 | } 152 | 153 | func doAction(log logr.Logger, name, namespace string, params map[string]string, spec Spec) (rsp ImplementRsp, err error) { 154 | ip, port, err := getService(name, namespace) 155 | if err != nil { 156 | log.Error(err, "install implement by service failed") 157 | return rsp, err 158 | } 159 | 160 | if resp, err := Do(spec.Method, fmt.Sprintf("http://%s:%d/%s", ip, port, spec.RelativePath), spec.Values, 161 | params); err != nil { 162 | log.Error(err, fmt.Sprintf("http %s error, ip:%s", spec.Method, ip)) 163 | return rsp, err 164 | } else { 165 | defer resp.Body.Close() 166 | var rsp ImplementRsp 167 | if content, err := ioutil.ReadAll(resp.Body); err != nil { 168 | log.Error(err, fmt.Sprintf("http %s read content error, ip:%s", spec.Method, ip)) 169 | return rsp, err 170 | } else if err := json.Unmarshal(content, &rsp); err != nil { 171 | log.Error(err, fmt.Sprintf("http %s unmarshal content error, ip:%s", spec.Method, ip)) 172 | return rsp, err 173 | } else { 174 | log.Info(fmt.Sprintf("http %s success, ip:%s, rsp:%s", spec.Method, ip, string(content))) 175 | return rsp, nil 176 | } 177 | } 178 | } 179 | 180 | func Do(method, url string, values url.Values, params map[string]string) (resp *http.Response, err error) { 181 | req, err := http.NewRequest(getHttpMethod(method), url, strings.NewReader(values.Encode())) 182 | if err != nil { 183 | return nil, err 184 | } 185 | req.Header.Set("Content-Type", "application/x-www-form-urlencoded") 186 | q := req.URL.Query() 187 | for k, v := range params { 188 | q.Add(k, v) 189 | } 190 | req.URL.RawQuery = q.Encode() 191 | return http.DefaultClient.Do(req) 192 | } 193 | 194 | func getHttpMethod(method string) string { 195 | switch strings.ToLower(method) { 196 | case strings.ToLower(http.MethodGet): 197 | return http.MethodGet 198 | case strings.ToLower(http.MethodDelete): 199 | return http.MethodDelete 200 | case strings.ToLower(http.MethodPut): 201 | return http.MethodPut 202 | default: 203 | return http.MethodPost 204 | } 205 | } 206 | -------------------------------------------------------------------------------- /pkg/implement/service/zz_generated.deepcopy.go: -------------------------------------------------------------------------------- 1 | // +build !ignore_autogenerated 2 | 3 | /* 4 | 5 | 6 | Licensed under the Apache License, Version 2.0 (the "License"); 7 | you may not use this file except in compliance with the License. 8 | You may obtain a copy of the License at 9 | 10 | http://www.apache.org/licenses/LICENSE-2.0 11 | 12 | Unless required by applicable law or agreed to in writing, software 13 | distributed under the License is distributed on an "AS IS" BASIS, 14 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 15 | See the License for the specific language governing permissions and 16 | limitations under the License. 17 | */ 18 | 19 | // Code generated by controller-gen. DO NOT EDIT. 20 | 21 | package service 22 | 23 | import ( 24 | "net/url" 25 | ) 26 | 27 | // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. 28 | func (in *Implement) DeepCopyInto(out *Implement) { 29 | *out = *in 30 | in.Install.DeepCopyInto(&out.Install) 31 | in.Uninstall.DeepCopyInto(&out.Uninstall) 32 | in.Upgrade.DeepCopyInto(&out.Upgrade) 33 | in.Recover.DeepCopyInto(&out.Recover) 34 | in.Status.DeepCopyInto(&out.Status) 35 | } 36 | 37 | // DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new ServiceImplement. 38 | func (in *Implement) DeepCopy() *Implement { 39 | if in == nil { 40 | return nil 41 | } 42 | out := new(Implement) 43 | in.DeepCopyInto(out) 44 | return out 45 | } 46 | 47 | // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. 48 | func (in *Spec) DeepCopyInto(out *Spec) { 49 | *out = *in 50 | if in.Values != nil { 51 | in, out := &in.Values, &out.Values 52 | *out = make(url.Values, len(*in)) 53 | for key, val := range *in { 54 | var outVal []string 55 | if val == nil { 56 | (*out)[key] = nil 57 | } else { 58 | in, out := &val, &outVal 59 | *out = make([]string, len(*in)) 60 | copy(*out, *in) 61 | } 62 | (*out)[key] = outVal 63 | } 64 | } 65 | } 66 | 67 | // DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new Spec. 68 | func (in *Spec) DeepCopy() *Spec { 69 | if in == nil { 70 | return nil 71 | } 72 | out := new(Spec) 73 | in.DeepCopyInto(out) 74 | return out 75 | } 76 | -------------------------------------------------------------------------------- /pkg/implement/zz_generated.deepcopy.go: -------------------------------------------------------------------------------- 1 | // +build !ignore_autogenerated 2 | 3 | /* 4 | 5 | 6 | Licensed under the Apache License, Version 2.0 (the "License"); 7 | you may not use this file except in compliance with the License. 8 | You may obtain a copy of the License at 9 | 10 | http://www.apache.org/licenses/LICENSE-2.0 11 | 12 | Unless required by applicable law or agreed to in writing, software 13 | distributed under the License is distributed on an "AS IS" BASIS, 14 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 15 | See the License for the specific language governing permissions and 16 | limitations under the License. 17 | */ 18 | 19 | // Code generated by controller-gen. DO NOT EDIT. 20 | 21 | package implement 22 | 23 | import ( 24 | "cloudnativeapp/clm/pkg/implement/helm" 25 | "cloudnativeapp/clm/pkg/implement/native" 26 | "cloudnativeapp/clm/pkg/implement/service" 27 | ) 28 | 29 | // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. 30 | func (in *Implement) DeepCopyInto(out *Implement) { 31 | *out = *in 32 | if in.LocalService != nil { 33 | in, out := &in.LocalService, &out.LocalService 34 | *out = new(service.Implement) 35 | (*in).DeepCopyInto(*out) 36 | } 37 | if in.Helm != nil { 38 | in, out := &in.Helm, &out.Helm 39 | *out = new(helm.Implement) 40 | **out = **in 41 | } 42 | if in.Native != nil { 43 | in, out := &in.Native, &out.Native 44 | *out = new(native.Implement) 45 | **out = **in 46 | } 47 | } 48 | 49 | // DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new Implement. 50 | func (in *Implement) DeepCopy() *Implement { 51 | if in == nil { 52 | return nil 53 | } 54 | out := new(Implement) 55 | in.DeepCopyInto(out) 56 | return out 57 | } 58 | -------------------------------------------------------------------------------- /pkg/plugin/plugin.go: -------------------------------------------------------------------------------- 1 | package plugin 2 | 3 | import ( 4 | "errors" 5 | ctrl "sigs.k8s.io/controller-runtime" 6 | ) 7 | 8 | type Iplugin interface { 9 | // Get the name, version of the plugin. 10 | Attributes() (string, string) 11 | // Install the plugin, return status and error. 12 | Install() (interface{}, error) 13 | // Uninstall the plugin, return status and error. 14 | Uninstall() (interface{}, error) 15 | // Recover the plugin, return status and error. 16 | DoRecover() (interface{}, error) 17 | // Upgrade the plugin, return status and error. 18 | DoUpgrade() (interface{}, error) 19 | // Get the status of plugin, return status and error. 20 | //GetStatus() (interface{}, error) 21 | // convert the phase and error to status of actual plugin, throw error when phase abnormal 22 | ConvertStatus(string, error) (interface{}, error) 23 | } 24 | 25 | // StatusSet : Set status to plugin. 26 | // Inputs: name, version, status. 27 | // Return: ok, error 28 | type StatusSet func(string, string, interface{}) (bool, error) 29 | 30 | // StatusGet : Get the status of the plugin. 31 | // Inputs: name, version. 32 | // Return: action needed, plugin phase got, error. 33 | type StatusGet func(string, string) (Action, string, error) 34 | 35 | type Action string 36 | 37 | const ( 38 | // Need to install the plugin. 39 | NeedInstall Action = "NeedInstall" 40 | // Need to recover plugin. 41 | NeedRecover Action = "NeedRecover" 42 | // Need to uninstall the plugin. 43 | NeedUninstall Action = "NeedUninstall" 44 | // 45 | NeedNothing Action = "NeedNothing" 46 | // Need to upgrade the plugin. 47 | NeedUpgrade Action = "NeedUpgrade" 48 | // Need to convert phase to plugin status. 49 | NeedConvert Action = "NeedConvert" 50 | ) 51 | 52 | var pluginLog = ctrl.Log.WithName("plugin") 53 | 54 | //CheckPlugins : Check the status of all plugins. 55 | //Return true when all status of plugins are ready. 56 | func CheckPlugins(ps []Iplugin, setStatus StatusSet, getStatus StatusGet) (bool, error) { 57 | ready := true 58 | var errs []error 59 | for _, p := range ps { 60 | name, version := p.Attributes() 61 | ok, e := CheckPlugin(p, setStatus, getStatus) 62 | if e != nil { 63 | pluginLog.Error(e, "check plugin error", "name", name, "version", version) 64 | errs = append(errs, e) 65 | ready = false 66 | } else if !ok { 67 | ready = false 68 | } 69 | } 70 | 71 | return ready, NormalizedErrors(errs) 72 | } 73 | 74 | //NormalizedErrors All errors normalized as one. 75 | func NormalizedErrors(errs []error) error { 76 | var msg string 77 | if len(errs) == 0 { 78 | return nil 79 | } 80 | if len(errs) == 1 { 81 | return errs[0] 82 | } 83 | for _, e := range errs { 84 | msg = msg + ";" + e.Error() 85 | } 86 | 87 | return errors.New(msg) 88 | } 89 | 90 | //CheckPlugin Check whether the plugin is ready. 91 | func CheckPlugin(c Iplugin, setStatus StatusSet, getStatus StatusGet) (ready bool, err error) { 92 | var status interface{} 93 | n, v := c.Attributes() 94 | act, phase, err := getStatus(n, v) 95 | switch act { 96 | case NeedInstall: 97 | status, err = c.Install() 98 | case NeedRecover: 99 | pluginLog.Info("try to restore plugin", "name", n, "version", v) 100 | status, err = c.DoRecover() 101 | case NeedUninstall: 102 | pluginLog.Info("try to uninstall plugin", "name", n, "version", v) 103 | status, err = c.Uninstall() 104 | case NeedUpgrade: 105 | pluginLog.Info("try to upgrade plugin", "name", n, "version", v) 106 | status, err = c.DoUpgrade() 107 | case NeedConvert: 108 | fallthrough 109 | default: 110 | pluginLog.Info("try to convert status directly", "name", n, "version", v) 111 | status, _ = c.ConvertStatus(phase, err) 112 | } 113 | 114 | ok, err := setStatus(n, v, status) 115 | if err != nil { 116 | pluginLog.Error(err, "set plugin status error", "name", n, "version", v) 117 | } 118 | return ok, err 119 | } 120 | -------------------------------------------------------------------------------- /pkg/probe/exec/exec.go: -------------------------------------------------------------------------------- 1 | package exec 2 | 3 | import ( 4 | "cloudnativeapp/clm/pkg/probe" 5 | "k8s.io/utils/exec" 6 | ) 7 | 8 | const ( 9 | maxReadLength = 10 * 1 << 10 // 10KB 10 | ) 11 | 12 | // New creates a Prober. 13 | func New() Prober { 14 | return execProber{} 15 | } 16 | 17 | // Prober is an interface defining the Probe object for container readiness/liveness checks. 18 | type Prober interface { 19 | Probe(e exec.Cmd) (probe.Result, string, error) 20 | } 21 | 22 | type execProber struct{} 23 | 24 | // Probe executes a command to check the liveness/readiness of container 25 | // from executing a command. Returns the Result status, command output, and 26 | // errors if any. 27 | func (pr execProber) Probe(e exec.Cmd) (probe.Result, string, error) { 28 | return probe.Success, string(""), nil 29 | } 30 | -------------------------------------------------------------------------------- /pkg/probe/http/http.go: -------------------------------------------------------------------------------- 1 | package http 2 | 3 | import ( 4 | "cloudnativeapp/clm/pkg/probe" 5 | "cloudnativeapp/clm/pkg/utils" 6 | "crypto/tls" 7 | "encoding/json" 8 | "errors" 9 | "fmt" 10 | "net/http" 11 | "net/url" 12 | "strings" 13 | "time" 14 | 15 | utilnet "k8s.io/apimachinery/pkg/util/net" 16 | "k8s.io/component-base/version" 17 | 18 | utilio "k8s.io/utils/io" 19 | ) 20 | 21 | const ( 22 | maxRespBodyLength = 10 * 1 << 10 // 10KB 23 | ) 24 | 25 | type ResponseData struct { 26 | Code int `json:"code"` 27 | Msg string `json:"msg"` 28 | Data interface{} `json:"data"` 29 | Success bool `json:"success"` 30 | } 31 | 32 | // New creates Prober that will skip TLS verification while probing. 33 | // followNonLocalRedirects configures whether the prober should follow redirects to a different hostname. 34 | // If disabled, redirects to other hosts will trigger a warning result. 35 | func New(followNonLocalRedirects bool) Prober { 36 | tlsConfig := &tls.Config{InsecureSkipVerify: true} 37 | return NewWithTLSConfig(tlsConfig, followNonLocalRedirects) 38 | } 39 | 40 | // NewWithTLSConfig takes tls config as parameter. 41 | // followNonLocalRedirects configures whether the prober should follow redirects to a different hostname. 42 | // If disabled, redirects to other hosts will trigger a warning result. 43 | func NewWithTLSConfig(config *tls.Config, followNonLocalRedirects bool) Prober { 44 | // We do not want the probe use node's local proxy set. 45 | transport := utilnet.SetTransportDefaults( 46 | &http.Transport{ 47 | TLSClientConfig: config, 48 | DisableKeepAlives: true, 49 | Proxy: http.ProxyURL(nil), 50 | }) 51 | return httpProber{transport, followNonLocalRedirects} 52 | } 53 | 54 | // Prober is an interface that defines the Probe function for doing HTTP readiness/liveness checks. 55 | type Prober interface { 56 | Probe(url *url.URL, headers http.Header, timeout time.Duration) (probe.Result, string, error) 57 | } 58 | 59 | type httpProber struct { 60 | transport *http.Transport 61 | followNonLocalRedirects bool 62 | } 63 | 64 | // Probe returns a ProbeRunner capable of running an HTTP check. 65 | func (pr httpProber) Probe(url *url.URL, headers http.Header, timeout time.Duration) (probe.Result, string, error) { 66 | client := &http.Client{ 67 | Timeout: timeout, 68 | Transport: pr.transport, 69 | CheckRedirect: redirectChecker(pr.followNonLocalRedirects), 70 | } 71 | return DoHTTPProbe(url, headers, client) 72 | } 73 | 74 | // GetHTTPInterface is an interface for making HTTP requests, that returns a response and error. 75 | type GetHTTPInterface interface { 76 | Do(req *http.Request) (*http.Response, error) 77 | } 78 | 79 | // DoHTTPProbe checks if a GET request to the url succeeds. 80 | // If the HTTP response code is successful (i.e. 400 > code >= 200), it returns Success. 81 | // If the HTTP response code is unsuccessful or HTTP communication fails, it returns Failure. 82 | // This is exported because some other packages may want to do direct HTTP probes. 83 | func DoHTTPProbe(url *url.URL, headers http.Header, client GetHTTPInterface) (probe.Result, string, error) { 84 | req, err := http.NewRequest("GET", url.String(), nil) 85 | if err != nil { 86 | // Convert errors into failures to catch timeouts. 87 | return probe.Failure, err.Error(), nil 88 | } 89 | if _, ok := headers["User-Agent"]; !ok { 90 | if headers == nil { 91 | headers = http.Header{} 92 | } 93 | // explicitly set User-Agent so it's not set to default Go value 94 | v := version.Get() 95 | headers.Set("User-Agent", fmt.Sprintf("kube-probe/%s.%s", v.Major, v.Minor)) 96 | } 97 | req.Header = headers 98 | if headers.Get("Host") != "" { 99 | req.Host = headers.Get("Host") 100 | } 101 | res, err := client.Do(req) 102 | if err != nil { 103 | // Convert errors into failures to catch timeouts. 104 | return probe.Failure, err.Error(), nil 105 | } 106 | defer res.Body.Close() 107 | b, err := utilio.ReadAtMost(res.Body, maxRespBodyLength) 108 | if err != nil { 109 | if err == utilio.ErrLimitReached { 110 | probe.PLog.V(utils.Warn).Info(fmt.Sprintf("Non fatal body truncation for %s, Response: %v", url.String(), *res)) 111 | } else { 112 | return probe.Failure, "", err 113 | } 114 | } 115 | body := string(b) 116 | if res.StatusCode >= http.StatusOK && res.StatusCode < http.StatusBadRequest { 117 | var out string 118 | data := ResponseData{} 119 | err := json.Unmarshal(b, &data) 120 | if err == nil { 121 | if data.Data != nil { 122 | if _, ok := data.Data.(string); ok { 123 | out = strings.ToLower(data.Data.(string)) 124 | } 125 | } 126 | } else { 127 | probe.PLog.V(utils.Warn).Info("unmarshal error", "error", err) 128 | out = body 129 | } 130 | if res.StatusCode >= http.StatusMultipleChoices { // Redirect 131 | probe.PLog.V(utils.Warn).Info(fmt.Sprintf("Probe terminated redirects for %s, Response: %v", url.String(), *res)) 132 | 133 | return probe.Warning, out, nil 134 | } 135 | probe.PLog.V(utils.Info).Info(fmt.Sprintf("Probe succeeded for %s, Response: %v", url.String(), *res)) 136 | return probe.Success, out, nil 137 | } 138 | probe.PLog.V(utils.Warn).Info(fmt.Sprintf("Probe failed for %s with request headers %v, response body: %v", url.String(), headers, body)) 139 | return probe.Failure, fmt.Sprintf("HTTP probe failed with statuscode: %d", res.StatusCode), nil 140 | } 141 | 142 | func redirectChecker(followNonLocalRedirects bool) func(*http.Request, []*http.Request) error { 143 | if followNonLocalRedirects { 144 | return nil // Use the default http client checker. 145 | } 146 | 147 | return func(req *http.Request, via []*http.Request) error { 148 | if req.URL.Hostname() != via[0].URL.Hostname() { 149 | return http.ErrUseLastResponse 150 | } 151 | // Default behavior: stop after 10 redirects. 152 | if len(via) >= 10 { 153 | return errors.New("stopped after 10 redirects") 154 | } 155 | return nil 156 | } 157 | } 158 | -------------------------------------------------------------------------------- /pkg/probe/http/http_test.go: -------------------------------------------------------------------------------- 1 | package http 2 | 3 | import ( 4 | "encoding/json" 5 | "log" 6 | "testing" 7 | ) 8 | 9 | func TestDoHTTPProbe(t *testing.T) { 10 | test := "{\"code\":200,\"msg\":\"\",\"data\":\"Abnormal\",\"success\":true}" 11 | data := ResponseData{} 12 | err := json.Unmarshal([]byte(test), &data) 13 | if err != nil { 14 | log.Printf("%v", err) 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /pkg/probe/probe.go: -------------------------------------------------------------------------------- 1 | package probe 2 | 3 | import ( 4 | "k8s.io/apimachinery/pkg/util/intstr" 5 | ctrl "sigs.k8s.io/controller-runtime" 6 | ) 7 | 8 | // Result is a string used to handle the results for probing container readiness/liveness 9 | type Result string 10 | 11 | const ( 12 | // Success Result 13 | Success Result = "success" 14 | // Warning Result. Logically success, but with additional debugging information attached. 15 | Warning Result = "warning" 16 | // Failure Result 17 | Failure Result = "failure" 18 | // Unknown Result 19 | Unknown Result = "unknown" 20 | ) 21 | 22 | type Probe struct { 23 | RecoverThreshold int `json:"recoverThreshold,omitempty"` 24 | SuccessThreshold int `json:"successThreshold,omitempty"` 25 | FailureThreshold int `json:"failureThreshold,omitempty"` 26 | PeriodSeconds int `json:"periodSeconds,omitempty"` 27 | TimeoutSeconds int `json:"timeoutSeconds,omitempty"` 28 | Handler `json:",inline"` 29 | } 30 | 31 | type Handler struct { 32 | // HTTPGet specifies the http request to perform. 33 | // +optional 34 | HTTPGet *HTTPGetAction `json:"httpGet,omitempty" protobuf:"bytes,2,opt,name=httpGet"` 35 | // TCPSocket specifies an action involving a TCP port. 36 | // TCP hooks not yet supported 37 | // TODO: implement a realistic TCP lifecycle hook 38 | // +optional 39 | TCPSocket *TCPSocketAction `json:"tcpSocket,omitempty" protobuf:"bytes,3,opt,name=tcpSocket"` 40 | } 41 | 42 | type HTTPGetAction struct { 43 | // Path to access on the HTTP server. 44 | // +optional 45 | Path string `json:"path,omitempty" protobuf:"bytes,1,opt,name=path"` 46 | // Name or number of the port to access on the container. 47 | // Number must be in the range 1 to 65535. 48 | // Name must be an IANA_SVC_NAME. 49 | Port intstr.IntOrString `json:"port" protobuf:"bytes,2,opt,name=port"` 50 | // Host name to connect to, defaults to the pod IP. You probably want to set 51 | // "Host" in httpHeaders instead. 52 | // +optional 53 | Host string `json:"host,omitempty" protobuf:"bytes,3,opt,name=host"` 54 | // Optional: Set namespace when host as service name. 55 | // +optional 56 | Namespace string `json:"namespace,omitempty" protobuf:"bytes,2,opt,name=namespace"` 57 | // Scheme to use for connecting to the host. 58 | // Defaults to HTTP. 59 | // +optional 60 | Scheme URIScheme `json:"scheme,omitempty" protobuf:"bytes,4,opt,name=scheme,casttype=URIScheme"` 61 | // Custom headers to set in the request. HTTP allows repeated headers. 62 | // +optional 63 | HTTPHeaders []HTTPHeader `json:"httpHeaders,omitempty" protobuf:"bytes,5,rep,name=httpHeaders"` 64 | } 65 | 66 | // TCPSocketAction describes an action based on opening a socket 67 | type TCPSocketAction struct { 68 | // Number or name of the port to access on the container. 69 | // Number must be in the range 1 to 65535. 70 | // Name must be an IANA_SVC_NAME. 71 | Port intstr.IntOrString `json:"port" protobuf:"bytes,1,opt,name=port"` 72 | // Optional: Host name to connect to. 73 | // +optional 74 | Host string `json:"host,omitempty" protobuf:"bytes,2,opt,name=host"` 75 | // Optional: Set namespace when host as service name. 76 | // +optional 77 | Namespace string `json:"namespace,omitempty" protobuf:"bytes,2,opt,name=namespace"` 78 | } 79 | 80 | type HTTPHeader struct { 81 | // The header field name 82 | Name string `json:"name" protobuf:"bytes,1,opt,name=name"` 83 | // The header field value 84 | Value string `json:"value" protobuf:"bytes,2,opt,name=value"` 85 | } 86 | 87 | type URIScheme string 88 | 89 | var PLog = ctrl.Log.WithName("probe") 90 | -------------------------------------------------------------------------------- /pkg/probe/tcp/tcp.go: -------------------------------------------------------------------------------- 1 | package tcp 2 | 3 | import ( 4 | "cloudnativeapp/clm/pkg/probe" 5 | "cloudnativeapp/clm/pkg/utils" 6 | "net" 7 | "strconv" 8 | "time" 9 | ) 10 | 11 | // New creates Prober. 12 | func New() Prober { 13 | return tcpProber{} 14 | } 15 | 16 | // Prober is an interface that defines the Probe function for doing TCP readiness/liveness checks. 17 | type Prober interface { 18 | Probe(host string, port int, timeout time.Duration) (probe.Result, string, error) 19 | } 20 | 21 | type tcpProber struct{} 22 | 23 | // Probe returns a ProbeRunner capable of running an TCP check. 24 | func (pr tcpProber) Probe(host string, port int, timeout time.Duration) (probe.Result, string, error) { 25 | return DoTCPProbe(net.JoinHostPort(host, strconv.Itoa(port)), timeout) 26 | } 27 | 28 | // DoTCPProbe checks that a TCP socket to the address can be opened. 29 | // If the socket can be opened, it returns Success 30 | // If the socket fails to open, it returns Failure. 31 | // This is exported because some other packages may want to do direct TCP probes. 32 | func DoTCPProbe(addr string, timeout time.Duration) (probe.Result, string, error) { 33 | conn, err := net.DialTimeout("tcp", addr, timeout) 34 | if err != nil { 35 | // Convert errors to failures to handle timeouts. 36 | return probe.Failure, err.Error(), nil 37 | } 38 | err = conn.Close() 39 | if err != nil { 40 | probe.PLog.V(utils.Warn).Info("Unexpected error closing TCP probe socket: %v (%#v)", err, err) 41 | } 42 | return probe.Success, "ready", nil 43 | } 44 | -------------------------------------------------------------------------------- /pkg/probe/zz_generated.deepcopy.go: -------------------------------------------------------------------------------- 1 | // +build !ignore_autogenerated 2 | 3 | /* 4 | 5 | 6 | Licensed under the Apache License, Version 2.0 (the "License"); 7 | you may not use this file except in compliance with the License. 8 | You may obtain a copy of the License at 9 | 10 | http://www.apache.org/licenses/LICENSE-2.0 11 | 12 | Unless required by applicable law or agreed to in writing, software 13 | distributed under the License is distributed on an "AS IS" BASIS, 14 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 15 | See the License for the specific language governing permissions and 16 | limitations under the License. 17 | */ 18 | 19 | // Code generated by controller-gen. DO NOT EDIT. 20 | 21 | package probe 22 | 23 | // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. 24 | func (in *HTTPGetAction) DeepCopyInto(out *HTTPGetAction) { 25 | *out = *in 26 | out.Port = in.Port 27 | if in.HTTPHeaders != nil { 28 | in, out := &in.HTTPHeaders, &out.HTTPHeaders 29 | *out = make([]HTTPHeader, len(*in)) 30 | copy(*out, *in) 31 | } 32 | } 33 | 34 | // DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new HTTPGetAction. 35 | func (in *HTTPGetAction) DeepCopy() *HTTPGetAction { 36 | if in == nil { 37 | return nil 38 | } 39 | out := new(HTTPGetAction) 40 | in.DeepCopyInto(out) 41 | return out 42 | } 43 | 44 | // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. 45 | func (in *HTTPHeader) DeepCopyInto(out *HTTPHeader) { 46 | *out = *in 47 | } 48 | 49 | // DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new HTTPHeader. 50 | func (in *HTTPHeader) DeepCopy() *HTTPHeader { 51 | if in == nil { 52 | return nil 53 | } 54 | out := new(HTTPHeader) 55 | in.DeepCopyInto(out) 56 | return out 57 | } 58 | 59 | // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. 60 | func (in *Handler) DeepCopyInto(out *Handler) { 61 | *out = *in 62 | if in.HTTPGet != nil { 63 | in, out := &in.HTTPGet, &out.HTTPGet 64 | *out = new(HTTPGetAction) 65 | (*in).DeepCopyInto(*out) 66 | } 67 | if in.TCPSocket != nil { 68 | in, out := &in.TCPSocket, &out.TCPSocket 69 | *out = new(TCPSocketAction) 70 | **out = **in 71 | } 72 | } 73 | 74 | // DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new Handler. 75 | func (in *Handler) DeepCopy() *Handler { 76 | if in == nil { 77 | return nil 78 | } 79 | out := new(Handler) 80 | in.DeepCopyInto(out) 81 | return out 82 | } 83 | 84 | // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. 85 | func (in *Probe) DeepCopyInto(out *Probe) { 86 | *out = *in 87 | in.Handler.DeepCopyInto(&out.Handler) 88 | } 89 | 90 | // DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new Probe. 91 | func (in *Probe) DeepCopy() *Probe { 92 | if in == nil { 93 | return nil 94 | } 95 | out := new(Probe) 96 | in.DeepCopyInto(out) 97 | return out 98 | } 99 | 100 | // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. 101 | func (in *TCPSocketAction) DeepCopyInto(out *TCPSocketAction) { 102 | *out = *in 103 | out.Port = in.Port 104 | } 105 | 106 | // DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new TCPSocketAction. 107 | func (in *TCPSocketAction) DeepCopy() *TCPSocketAction { 108 | if in == nil { 109 | return nil 110 | } 111 | out := new(TCPSocketAction) 112 | in.DeepCopyInto(out) 113 | return out 114 | } 115 | -------------------------------------------------------------------------------- /pkg/prober/prober.go: -------------------------------------------------------------------------------- 1 | package prober 2 | 3 | import ( 4 | "cloudnativeapp/clm/pkg/probe" 5 | execprobe "cloudnativeapp/clm/pkg/probe/exec" 6 | httpprobe "cloudnativeapp/clm/pkg/probe/http" 7 | tcpprobe "cloudnativeapp/clm/pkg/probe/tcp" 8 | "cloudnativeapp/clm/pkg/utils" 9 | "fmt" 10 | "k8s.io/apimachinery/pkg/util/intstr" 11 | "net" 12 | "net/http" 13 | "net/url" 14 | ctrl "sigs.k8s.io/controller-runtime" 15 | "strconv" 16 | "strings" 17 | "time" 18 | ) 19 | 20 | var probeLog = ctrl.Log.WithName("probe") 21 | 22 | type Prober struct { 23 | exec execprobe.Prober 24 | http httpprobe.Prober 25 | tcp tcpprobe.Prober 26 | } 27 | 28 | func NewProber() *Prober { 29 | const followNonLocalRedirects = false 30 | return &Prober{ 31 | exec: execprobe.New(), 32 | http: httpprobe.New(followNonLocalRedirects), 33 | tcp: tcpprobe.New(), 34 | } 35 | } 36 | 37 | func (pb *Prober) RunProbeWithRetries(retries int, p *probe.Probe) (probe.Result, string, error) { 38 | var err error 39 | var result probe.Result 40 | var output string 41 | for i := 0; i < retries; i++ { 42 | result, output, err = pb.runProbe(p) 43 | if err == nil { 44 | return result, output, nil 45 | } 46 | } 47 | return result, output, err 48 | } 49 | 50 | func (pb *Prober) runProbe(p *probe.Probe) (probe.Result, string, error) { 51 | timeout := time.Duration(p.TimeoutSeconds) * time.Second 52 | if p.Handler.HTTPGet != nil { 53 | scheme := strings.ToLower(string(p.Handler.HTTPGet.Scheme)) 54 | host := p.Handler.HTTPGet.Host 55 | if len(p.Handler.HTTPGet.Namespace) > 0 { 56 | host = host + "." + p.Handler.HTTPGet.Namespace 57 | } 58 | port, err := extractPort(p.Handler.HTTPGet.Port) 59 | if err != nil { 60 | return probe.Unknown, "", err 61 | } 62 | path := p.Handler.HTTPGet.Path 63 | probeLog.V(utils.Debug).Info(fmt.Sprintf("HTTP-Probe Host: %v://%v, Port: %v, Path: %v", scheme, host, port, path)) 64 | url := formatURL(scheme, host, port, path) 65 | headers := buildHeader(p.Handler.HTTPGet.HTTPHeaders) 66 | probeLog.V(utils.Debug).Info(fmt.Sprintf("HTTP-Probe Headers: %v", headers)) 67 | return pb.http.Probe(url, headers, timeout) 68 | 69 | } 70 | if p.Handler.TCPSocket != nil { 71 | port, err := extractPort(p.Handler.TCPSocket.Port) 72 | if err != nil { 73 | return probe.Unknown, "", err 74 | } 75 | host := p.Handler.TCPSocket.Host 76 | if len(p.Handler.TCPSocket.Namespace) > 0 { 77 | host = host + "." + p.Handler.TCPSocket.Namespace 78 | } 79 | probeLog.V(utils.Debug).Info(fmt.Sprintf("TCP-Probe Host: %v, Port: %v, Timeout: %v", host, port, timeout)) 80 | return pb.tcp.Probe(host, port, timeout) 81 | } 82 | probeLog.V(utils.Warn).Info("Failed to find probe builder") 83 | return probe.Unknown, "", fmt.Errorf("missing probe handler") 84 | } 85 | 86 | func extractPort(param intstr.IntOrString) (int, error) { 87 | port := -1 88 | switch param.Type { 89 | case intstr.Int: 90 | port = param.IntValue() 91 | case intstr.String: 92 | p, err := strconv.Atoi(param.StrVal) 93 | if err != nil { 94 | return -1, err 95 | } 96 | port = p 97 | default: 98 | return port, fmt.Errorf("intOrString had no kind: %+v", param) 99 | } 100 | if port > 0 && port < 65536 { 101 | return port, nil 102 | } 103 | return port, fmt.Errorf("invalid port number: %v", port) 104 | } 105 | 106 | func formatURL(scheme string, host string, port int, path string) *url.URL { 107 | u, err := url.Parse(path) 108 | // Something is busted with the path, but it's too late to reject it. Pass it along as is. 109 | if err != nil { 110 | u = &url.URL{ 111 | Path: path, 112 | } 113 | } 114 | u.Scheme = scheme 115 | u.Host = net.JoinHostPort(host, strconv.Itoa(port)) 116 | return u 117 | } 118 | 119 | func buildHeader(headerList []probe.HTTPHeader) http.Header { 120 | headers := make(http.Header) 121 | for _, header := range headerList { 122 | headers[header.Name] = append(headers[header.Name], header.Value) 123 | } 124 | return headers 125 | } 126 | -------------------------------------------------------------------------------- /pkg/recover/recover.go: -------------------------------------------------------------------------------- 1 | package recover 2 | 3 | import ( 4 | "cloudnativeapp/clm/pkg/cliruntime" 5 | "cloudnativeapp/clm/pkg/utils" 6 | "github.com/go-logr/logr" 7 | "github.com/pkg/errors" 8 | ) 9 | 10 | type Recover struct { 11 | Job string `json:"job,omitempty"` 12 | } 13 | 14 | func (r Recover) DoRecover(log logr.Logger) error { 15 | if len(r.Job) != 0 { 16 | n := cliruntime.NewApplyOptions(nil, r.Job, false) 17 | err := n.Run() 18 | if err != nil { 19 | log.Error(err, "do recover by job failed") 20 | return err 21 | } 22 | log.V(utils.Info).Info("run recover job succeed") 23 | return nil 24 | } 25 | return errors.New("no recover method found") 26 | } 27 | -------------------------------------------------------------------------------- /pkg/utils/utils.go: -------------------------------------------------------------------------------- 1 | package utils 2 | 3 | import ( 4 | "strconv" 5 | "strings" 6 | ) 7 | 8 | const ( 9 | ImplementNotFound = "implement not found" 10 | ModuleStateAbnormal = "module state abnormal" 11 | ReleaseStateAbnormal = "release state abnormal" 12 | PreCheckWaiting = "preCheck waiting" 13 | DependencyStateAbnormal = "dependency state abnormal" 14 | DependencyVersionMismatch = "dependency version mismatch absent" 15 | DependencyAbsentError = "dependency absent" 16 | DependencyPullError = "dependency pull error" 17 | DependencyWaiting = "dependency absent waiting" 18 | ) 19 | 20 | const ( 21 | Error int = iota - 2 22 | Warn 23 | Info 24 | Debug 25 | ) 26 | 27 | func Contains(list []string, s string) bool { 28 | for _, v := range list { 29 | if v == s { 30 | return true 31 | } 32 | } 33 | return false 34 | } 35 | 36 | func Remove(list []string, s string) []string { 37 | for i, v := range list { 38 | if v == s { 39 | list = append(list[:i], list[i+1:]...) 40 | } 41 | } 42 | return list 43 | } 44 | 45 | func IgnoreWaitingErr(err error) error { 46 | if err == nil { 47 | return nil 48 | } 49 | e := err.Error() 50 | if e == ImplementNotFound || 51 | e == PreCheckWaiting || 52 | e == DependencyWaiting { 53 | return nil 54 | } 55 | return err 56 | } 57 | 58 | func IgnoreKnownErr(err error) error { 59 | if err == nil { 60 | return nil 61 | } 62 | e := err.Error() 63 | if e == ImplementNotFound || 64 | e == ModuleStateAbnormal || 65 | e == ReleaseStateAbnormal || 66 | e == PreCheckWaiting || 67 | e == DependencyStateAbnormal || 68 | e == DependencyVersionMismatch || 69 | e == DependencyAbsentError || 70 | e == DependencyPullError || 71 | e == DependencyWaiting { 72 | return nil 73 | } 74 | return err 75 | } 76 | 77 | //VersionMatch : min <= current < max 78 | func VersionMatch(current, min, max string) bool { 79 | if current == min || current == max { 80 | return true 81 | } 82 | if len(min) > 0 && !versionEqualOrNewer(current, min) { 83 | return false 84 | } 85 | if len(max) > 0 && versionEqualOrNewer(current, max) { 86 | return false 87 | } 88 | return true 89 | } 90 | 91 | func versionEqualOrNewer(v1, v2 string) bool { 92 | nslice := strings.Split(v1, ".") 93 | oslice := strings.Split(v2, ".") 94 | var maxlen int 95 | if len(nslice) > len(oslice) { 96 | maxlen = len(oslice) 97 | } else { 98 | maxlen = len(nslice) 99 | } 100 | 101 | for i := 0; i < maxlen; i++ { 102 | tmpn, err := strconv.Atoi(nslice[i]) 103 | if err != nil { 104 | return true 105 | } 106 | 107 | tmpo, err := strconv.Atoi(oslice[i]) 108 | if err != nil { 109 | return true 110 | } 111 | 112 | if tmpn > tmpo { 113 | return true 114 | } 115 | } 116 | 117 | return len(nslice) > len(oslice) 118 | } 119 | -------------------------------------------------------------------------------- /pkg/utils/utils_test.go: -------------------------------------------------------------------------------- 1 | package utils 2 | 3 | import "testing" 4 | 5 | func TestVersionMatch(t *testing.T) { 6 | ok := VersionMatch("1.0.6", "1.0.1", "1.0.5") 7 | if ok { 8 | t.Log("ok") 9 | } else { 10 | t.Log("not ok") 11 | } 12 | } 13 | --------------------------------------------------------------------------------