├── .gitattributes ├── .gitignore ├── main.go ├── Dockerfile ├── pkg ├── nodesCheck.go ├── execute.go ├── rbac.go ├── networkCheck.go └── ingressCheck.go ├── go.mod ├── README.md ├── kubernetes └── cranehook.yaml ├── Jenkinsfile ├── LICENSE └── go.sum /.gitattributes: -------------------------------------------------------------------------------- 1 | # Auto detect text files and perform LF normalization 2 | * text=auto 3 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # If you prefer the allow list template instead of the deny list, see community template: 2 | # https://github.com/github/gitignore/blob/main/community/Golang/Go.AllowList.gitignore 3 | # 4 | # Binaries for programs and plugins 5 | *.exe 6 | *.exe~ 7 | *.dll 8 | *.so 9 | *.dylib 10 | local_build* 11 | # Test binary, built with `go test -c` 12 | *.test 13 | 14 | # Output of the go coverage tool, specifically when used with LiteIDE 15 | *.out 16 | 17 | # Dependency directories (remove the comment below to include it) 18 | # vendor/ 19 | 20 | # Go workspace file 21 | go.work 22 | -------------------------------------------------------------------------------- /main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "fmt" 5 | "os" 6 | "time" 7 | 8 | "github.com/Blazemeter/crane-hook/pkg" 9 | ) 10 | 11 | func main() { 12 | statusErr := &pkg.StatusError{} 13 | pkg.Execute(statusErr) 14 | 15 | err := pkg.Consolidation(statusErr) 16 | if err != nil { 17 | fmt.Printf("\n\n[%s][FAIL] %v\n", time.Now().Format("2006-01-02 15:04:05"), err) 18 | os.Stdout.Sync() // flush output 19 | os.Exit(1) 20 | } 21 | fmt.Printf("\n\n[%s][PASS] All checks passed successfully, Private Location ready to accept Blazemeter Deployments\n", time.Now().Format("2006-01-02 15:04:05")) 22 | os.Exit(0) 23 | } 24 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | 2 | FROM --platform=linux/amd64 alpine:latest 3 | 4 | WORKDIR /usr/local/bin 5 | COPY cranehook . 6 | 7 | # Set environment variables 8 | ENV WORKING_NAMESPACE="" 9 | ENV ROLE_NAME="" 10 | ENV ROLE_BINDING_NAME="" 11 | ENV SERVICE_ACCOUNT_NAME="" 12 | ENV KUBERNETES_WEB_EXPOSE_TYPE="" 13 | # ENV SV_ENABLE="" 14 | ENV KUBERNETES_ISTIO_GATEWAY_NAME="" 15 | ENV DOCKER_REGISTRY="" 16 | ENV KUBERNETES_WEB_EXPOSE_TLS_SECRET_NAME="" 17 | ENV HTTP_PROXY="" 18 | ENV HTTPS_PROXY="" 19 | ENV NO_PROXY="" 20 | 21 | # Create a non-root user and group 22 | RUN addgroup -g 1337 -S appgroup && adduser -u 1337 -S appuser -G appgroup \ 23 | && chown appuser:appgroup /usr/local/bin/cranehook \ 24 | && chmod +x /usr/local/bin/cranehook 25 | 26 | USER appuser 27 | 28 | CMD ["/usr/local/bin/cranehook"] 29 | -------------------------------------------------------------------------------- /pkg/nodesCheck.go: -------------------------------------------------------------------------------- 1 | package pkg 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | "time" 7 | 8 | metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" 9 | ) 10 | 11 | func (statusErr *StatusError) listNodesDetails(cs *ClientSet) { 12 | clientset := cs.clientset 13 | // Configure slog logger 14 | //statusErr := &StatusError{} 15 | //logger := slog.New(slog.NewJSONHandler(os.Stdout, nil)) 16 | 17 | // Infinite loop to get node details every 30 seconds, until the pod/program is terminated. 18 | nodes, err := clientset.CoreV1().Nodes().List(context.TODO(), metav1.ListOptions{}) 19 | if err != nil { 20 | statusErr.NodeStatus = err 21 | //fmt.Println(statusErr.NodeStatus) 22 | } 23 | for i, nd := range nodes.Items { 24 | var errs []string 25 | 26 | cpu := nd.Status.Capacity.Cpu().MilliValue() 27 | mem := nd.Status.Capacity.Memory().Value() 28 | storage := nd.Status.Capacity.StorageEphemeral().Value() 29 | memMB := mem / (1024 * 1024) 30 | storageMB := storage / (1024 * 1024) 31 | if cpu < 2000 { 32 | statusErr.NodeResourceStatus = append(statusErr.NodeResourceStatus, map[string]error{ 33 | fmt.Sprintf("cpu node %d", i): fmt.Errorf("insufficient %d", cpu), 34 | }) 35 | errs = append(errs, fmt.Sprintf("CPU: %d", cpu)) 36 | } 37 | if memMB < 4096 { 38 | statusErr.NodeResourceStatus = append(statusErr.NodeResourceStatus, map[string]error{ 39 | fmt.Sprintf("memory node %d", i): fmt.Errorf("insufficient %d", memMB), 40 | }) 41 | errs = append(errs, fmt.Sprintf("MEM: %d MB", memMB)) 42 | } 43 | if storageMB < 64 { 44 | statusErr.NodeResourceStatus = append(statusErr.NodeResourceStatus, map[string]error{ 45 | fmt.Sprintf("storage node %d", i): fmt.Errorf("insufficient %d", storageMB), 46 | }) 47 | errs = append(errs, fmt.Sprintf("STORAGE: %d MB", storageMB)) 48 | } 49 | 50 | if len(errs) > 0 { 51 | fmt.Printf("\n[%s][error] node %d insufficient resources: %s", time.Now().Format("2006-01-02 15:04:05"), i+1, errs) 52 | } else { 53 | fmt.Printf("\n[%s][INFO] Node: %d, CPU: %d, MEM: %d MB, Storage: %d MB", time.Now().Format("2006-01-02 15:04:05"), i+1, cpu, memMB, storageMB) 54 | } 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/Blazemeter/crane-hook 2 | 3 | go 1.23.3 4 | 5 | require k8s.io/client-go v0.32.2 6 | 7 | require ( 8 | github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect 9 | github.com/emicklei/go-restful/v3 v3.11.0 // indirect 10 | github.com/fxamacker/cbor/v2 v2.7.0 // indirect 11 | github.com/go-logr/logr v1.4.2 // indirect 12 | github.com/go-openapi/jsonpointer v0.21.0 // indirect 13 | github.com/go-openapi/jsonreference v0.20.2 // indirect 14 | github.com/go-openapi/swag v0.23.0 // indirect 15 | github.com/gogo/protobuf v1.3.2 // indirect 16 | github.com/golang/protobuf v1.5.4 // indirect 17 | github.com/google/gnostic-models v0.6.8 // indirect 18 | github.com/google/go-cmp v0.6.0 // indirect 19 | github.com/google/gofuzz v1.2.0 // indirect 20 | github.com/google/uuid v1.6.0 // indirect 21 | github.com/josharian/intern v1.0.0 // indirect 22 | github.com/json-iterator/go v1.1.12 // indirect 23 | github.com/mailru/easyjson v0.7.7 // indirect 24 | github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect 25 | github.com/modern-go/reflect2 v1.0.2 // indirect 26 | github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect 27 | github.com/pkg/errors v0.9.1 // indirect 28 | github.com/x448/float16 v0.8.4 // indirect 29 | golang.org/x/net v0.30.0 // indirect 30 | golang.org/x/oauth2 v0.23.0 // indirect 31 | golang.org/x/sys v0.26.0 // indirect 32 | golang.org/x/term v0.25.0 // indirect 33 | golang.org/x/text v0.19.0 // indirect 34 | golang.org/x/time v0.7.0 // indirect 35 | google.golang.org/protobuf v1.35.1 // indirect 36 | gopkg.in/evanphx/json-patch.v4 v4.12.0 // indirect 37 | gopkg.in/inf.v0 v0.9.1 // indirect 38 | gopkg.in/yaml.v3 v3.0.1 // indirect 39 | k8s.io/api v0.32.2 // indirect 40 | k8s.io/apimachinery v0.32.2 // indirect 41 | k8s.io/klog/v2 v2.130.1 // indirect 42 | k8s.io/kube-openapi v0.0.0-20241105132330-32ad38e42d3f // indirect 43 | k8s.io/utils v0.0.0-20241104100929-3ea5e8cea738 // indirect 44 | sigs.k8s.io/json v0.0.0-20241010143419-9aa6b5e7a4b3 // indirect 45 | sigs.k8s.io/structured-merge-diff/v4 v4.4.2 // indirect 46 | sigs.k8s.io/yaml v1.4.0 // indirect 47 | ) 48 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # crane-hook 2 | 3 | A Kubernetes cluster requirements checker for Blazemeter Private Locations. This tool verifies node resources, network connectivity, RBAC, and ingress configuration to ensure your cluster is ready to deploy Blazemeter workloads 4 | 5 | ## Features 6 | 7 | - Checks all nodes in the cluster for CPU, memory, and storage capacity 8 | - Verifies network access to Blazemeter, Docker registry, and third-party endpoints 9 | - Validates Kubernetes RBAC roles and bindings 10 | - Confirms ingress or Istio configuration/deployment, gateway setup and TLS secret presence (for SV based OPLs) 11 | - Validates if a loadbalancer is configured correctly and accessible through an external IP (for SV based OPLs) 12 | - Designed to run as a Kubernetes Pod 13 | 14 | 15 | ## Usage 16 | 17 | ### [1.0] As a helm test hook 18 | 19 | This image is packaged as a test hook with [`helm-crane`](https://github.com/Blazemeter/helm-crane/releases) chart, versioned `1.4.0` and later. 20 | The test hook cal be executed by: 21 | ```sh 22 | helm test 23 | ``` 24 | It will automatically test the installation. 25 | 26 | **This is the only recommended method to execute the test hook. Running it manually or as an individual k8s pod may not produce the desired results.** 27 | 28 | See the [documentation](https://github.com/Blazemeter/helm-crane/blob/main/README.md) to learn more. 29 | 30 | 31 | ### [2.0] As a Kubernetes Pod 32 | 33 | If you prefer, you can deploy the test image manually as a Kubernetes pod. However, you will need to configure the environment variables and roles required to test the installation correctly. 34 | 35 | Download the manifest YAML, see [`kubernetes/cranehook.yaml`](kubernetes/cranehook.yaml) for an example manifest. 36 | 37 | 38 | **Update the ENV Variables in the manifest file**, here are the details of what those ENV Variables mean: 39 | 40 | - `WORKING_NAMESPACE`: Namespace in which the crane and it's resources are installed. 41 | - `ROLE_NAME`: Name of the Role that the crane serviceAccount is using 42 | - `ROLE_BINDING_NAME`: Name of the RoleBinding that is binding the role to the serviceAccount used by crane. 43 | - `SERVICE_ACCOUNT_NAME`: ServiceAccount used by crane, it is using the above roles for normal functioning of the crane/Blazemeter agent. 44 | - `DOCKER_REGISTRY`: (Optional) Docker registry URL to check (it should be your private container image registry in case of image override) 45 | - `KUBERNETES_WEB_EXPOSE_TYPE`: (Optional) INGRESS or ISTIO for Nginx or Istio type of ingress setup, required for Service virtualisation. 46 | - `KUBERNETES_WEB_EXPOSE_TLS_SECRET_NAME`: (Optional) TLS secret name for nginx/istio type ingress setup 47 | - `KUBERNETES_ISTIO_GATEWAY_NAME`: (Optional if using Istio) Gateway resource name 48 | - `HTTP_PROXY`, `HTTPS_PROXY`, `NO_PROXY`: (optional) Proxy settings, same as crane deployment manifest 49 | **Update other manifest configurations** 50 | 51 | - Update the namespace for all resources in the manifest to the correct one, that you are using to run crane. 52 | - In case of Istio based crane installation, please uncomment the istio related sections in the manifest file. 53 | 54 | Once the ENV variables are set, and configurations are updated, you can run the manifest yaml. 55 | 56 | ```sh 57 | kubectl apply -f cranehook.yaml 58 | ``` 59 | 60 | 61 | ## Output 62 | 63 | - `[INFO]` messages indicate successful checks 64 | - `[error]` messages indicate failed checks 65 | - Exit code 0: all checks passed 66 | - Exit code 1: one or more checks failed (the logs would list the failures/errors) 67 | -------------------------------------------------------------------------------- /kubernetes/cranehook.yaml: -------------------------------------------------------------------------------- 1 | # Source: helm-crane/templates/tests/testrbac.yaml 2 | apiVersion: rbac.authorization.k8s.io/v1 3 | kind: Role 4 | metadata: 5 | name: test-hookrole 6 | namespace: default 7 | rules: 8 | - apiGroups: ["rbac.authorization.k8s.io"] 9 | resources: ["rolebindings", "roles"] 10 | verbs: ["get", "list"] 11 | - apiGroups: [""] 12 | resources: ["secrets", "namespaces", "pods", "nodes", "services", "endpoints", "configmaps"] 13 | verbs: ["get", "list"] 14 | - apiGroups: ["networking.k8s.io"] 15 | resources: ["ingresses"] 16 | verbs: ["get", "list"] 17 | - apiGroups: ["networking.istio.io"] 18 | resources: ["virtualservices", "destinationrules", "gateways", "serviceentries"] 19 | verbs: ["get", "list"] 20 | --- 21 | # Source: helm-crane/templates/tests/testrbac.yaml 22 | apiVersion: rbac.authorization.k8s.io/v1 23 | kind: RoleBinding 24 | metadata: 25 | name: test-hookrole-binding-default 26 | namespace: default 27 | annotations: 28 | "helm.sh/hook": pre-install 29 | "helm.sh/hook-delete-policy": before-hook-creation 30 | subjects: 31 | - kind: ServiceAccount 32 | name: default 33 | namespace: default 34 | roleRef: 35 | kind: Role 36 | name: test-hookrole 37 | apiGroup: rbac.authorization.k8s.io 38 | 39 | 40 | ## The below commented section is for istio-based deployments. Uncommend both role and rolebindings. 41 | #--- 42 | #apiVersion: rbac.authorization.k8s.io/v1 43 | #kind: Role 44 | #metadata: 45 | # name: test-hookrole-istio 46 | # namespace: istio-system 47 | #rules: 48 | #- apiGroups: [""] 49 | # resources: ["secrets"] 50 | # verbs: ["get", "list"] 51 | #- apiGroups: ["networking.istio.io"] 52 | # resources: ["virtualservices", "destinationrules", "gateways", "serviceentries"] 53 | # verbs: ["get", "list"] 54 | #--- 55 | ## Source: helm-crane/templates/tests/testrbac.yaml 56 | #apiVersion: rbac.authorization.k8s.io/v1 57 | #kind: RoleBinding 58 | #metadata: 59 | # name: test-hookrole-istio-binding-default 60 | # namespace: istio-system 61 | #subjects: 62 | #- kind: ServiceAccount 63 | # name: default # or your custom SA name 64 | # namespace: default # your pod's namespace 65 | #roleRef: 66 | # kind: Role 67 | # name: test-hookrole-istio 68 | # apiGroup: rbac.authorization.k8s.io 69 | 70 | --- 71 | # Source: helm-crane/templates/tests/testhook.yaml 72 | apiVersion: v1 73 | kind: Pod 74 | metadata: 75 | name: cranehook 76 | namespace: default 77 | spec: 78 | serviceAccountName: default 79 | automountServiceAccountToken: true 80 | containers: 81 | - name: cranehook-ctr 82 | securityContext: 83 | runAsUser: 1337 84 | runAsGroup: 1337 85 | allowPrivilegeEscalation: false 86 | runAsNonRoot: true 87 | env: 88 | #Check ENV used in the ReadMe 89 | - name: WORKING_NAMESPACE 90 | value: 91 | - name: ROLE_NAME 92 | value: 93 | - name: ROLE_BINDING_NAME 94 | value: 95 | - name: SERVICE_ACCOUNT_NAME 96 | value: 97 | - name: KUBERNETES_WEB_EXPOSE_TYPE 98 | value: 99 | - name: DOCKER_REGISTRY 100 | value: "gcr.io/verdant-bulwark-278" 101 | - name: KUBERNETES_WEB_EXPOSE_TLS_SECRET_NAME 102 | value: 103 | image: "gcr.io/verdant-bulwark-278/cranehook:latest" 104 | imagePullPolicy: "Always" 105 | resources: 106 | requests: 107 | cpu: 100m 108 | memory: 256Mi 109 | limits: 110 | cpu: 200m 111 | memory: 512Mi 112 | volumeMounts: 113 | - name: testhook-tmp 114 | mountPath: /tmp/testhook 115 | volumes: 116 | - name: testhook-tmp 117 | emptyDir: {} 118 | restartPolicy: "Never" 119 | -------------------------------------------------------------------------------- /pkg/execute.go: -------------------------------------------------------------------------------- 1 | package pkg 2 | 3 | import ( 4 | "errors" 5 | "fmt" 6 | "os" 7 | "time" 8 | 9 | "k8s.io/client-go/kubernetes" 10 | "k8s.io/client-go/rest" 11 | ) 12 | 13 | //namespace := "default" // replace with your namespace 14 | 15 | type StatusError struct { 16 | NodeStatus error 17 | NodeResourceStatus []map[string]error 18 | BlazeNetworkStatus []map[string]error 19 | ImageRegistryNetworkStatus error 20 | ThirdPartyNetworkStatus []map[string]error 21 | IngressStatus error 22 | RBAC error 23 | } 24 | 25 | type ClientSet struct { 26 | clientset *kubernetes.Clientset 27 | } 28 | 29 | func (cs *ClientSet) getClientSet() { 30 | // Create a new Kubernetes client 31 | config, err := rest.InClusterConfig() 32 | if err != nil { 33 | panic(err.Error()) 34 | } 35 | clientset, err := kubernetes.NewForConfig(config) 36 | if err != nil { 37 | panic(err.Error()) 38 | } 39 | //return clientset 40 | cs.clientset = clientset 41 | } 42 | 43 | func Execute(statusError *StatusError) { 44 | // This is a test function to check if the package is working 45 | cs := &ClientSet{} 46 | cs.getClientSet() 47 | // statusError := &StatusError{} 48 | fmt.Printf("\n[%s][INFO] Starting the requirements check...", time.Now().Format("2006-01-02 15:04:05")) 49 | svEnabled := os.Getenv("KUBERNETES_WEB_EXPOSE_TYPE") 50 | statusError.networkCheckBlaze() 51 | statusError.networkCheckImageRegistry() 52 | statusError.networkCheckThirdParty() 53 | statusError.listNodesDetails(cs) 54 | statusError.rbacDefault(cs) 55 | if svEnabled != "" { 56 | statusError.checkIngress(cs) 57 | } 58 | 59 | } 60 | 61 | func Consolidation(statusErr *StatusError) error { 62 | // Check NodeStatus 63 | if statusErr.NodeStatus != nil { 64 | return errors.New("requirements check failed, check errors in logs") 65 | } 66 | 67 | // Check NodeResourceStatus 68 | for _, resourceStatus := range statusErr.NodeResourceStatus { 69 | for _, err := range resourceStatus { 70 | if err != nil { 71 | return errors.New("requirements check failed, check errors in logs") 72 | } 73 | } 74 | } 75 | 76 | // Check BlazeNetworkStatus 77 | for _, blazeStatus := range statusErr.BlazeNetworkStatus { 78 | for _, err := range blazeStatus { 79 | if err != nil { 80 | return errors.New("requirements check failed, check errors in logs") 81 | } 82 | } 83 | } 84 | 85 | // Check ImageRegistryNetworkStatus 86 | if statusErr.ImageRegistryNetworkStatus != nil { 87 | return errors.New("requirements check failed, check errors in logs") 88 | } 89 | 90 | // Check ThirdPartyNetworkStatus 91 | for _, thirdPartyStatus := range statusErr.ThirdPartyNetworkStatus { 92 | for _, err := range thirdPartyStatus { 93 | if err != nil { 94 | return errors.New("requirements check failed, check errors in logs") 95 | } 96 | } 97 | } 98 | // Check IngressAvailability 99 | if statusErr.IngressStatus != nil { 100 | return errors.New("requirements check failed, check errors in logs") 101 | } 102 | 103 | // Check RBAC requirements 104 | if statusErr.RBAC != nil { 105 | return errors.New("requirements check failed, check errors in logs") 106 | } 107 | 108 | return nil 109 | } 110 | 111 | // Helper functions: 112 | // contains checks if a string is present in a slice. 113 | func contains(slice []string, item string) bool { 114 | for _, s := range slice { 115 | if s == item { 116 | return true 117 | } 118 | } 119 | return false 120 | } 121 | 122 | // containsAll checks if all elements of `subset` are present in `set`. 123 | func containsAll(set []string, subset []string) bool { 124 | for _, sub := range subset { 125 | found := false 126 | for _, s := range set { 127 | if s == sub { 128 | found = true 129 | break 130 | } 131 | } 132 | if !found { 133 | //fmt.Printf("\n[%s]Missing required item: %q", sub) 134 | return false 135 | } 136 | } 137 | return true 138 | } 139 | 140 | //func dedup(slice []string) []string { 141 | // seen := make(map[string]struct{}) 142 | // var result []string 143 | // for _, s := range slice { 144 | // if _, ok := seen[s]; !ok { 145 | // seen[s] = struct{}{} 146 | // result = append(result, s) 147 | // } 148 | // } 149 | // return result 150 | //} 151 | 152 | func dedup(items []string) []string { 153 | seen := make(map[string]bool) 154 | var result []string 155 | for _, item := range items { 156 | if !seen[item] { 157 | seen[item] = true 158 | result = append(result, item) 159 | } 160 | } 161 | return result 162 | } 163 | -------------------------------------------------------------------------------- /pkg/rbac.go: -------------------------------------------------------------------------------- 1 | package pkg 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | "os" 7 | "time" 8 | 9 | metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" 10 | ) 11 | 12 | func (statusError *StatusError) rbacDefault(cs *ClientSet) { 13 | 14 | workingNs := os.Getenv("WORKING_NAMESPACE") 15 | if workingNs == "" { 16 | statusError.RBAC = fmt.Errorf("WORKING_NAMESPACE environment variable is not set") 17 | } 18 | // Read the role name from the ROLE_NAME environment variable 19 | roleName := os.Getenv("ROLE_NAME") 20 | if roleName == "" { 21 | statusError.RBAC = fmt.Errorf("role_name environment variable is not set") 22 | } 23 | 24 | requiredApiGroup := []string{"", "extensions", "apps", "batch"} 25 | requiredResources := []string{"pods", "services", "daemonsets", "replicasets", "deployments", "deployments/scale", "jobs", "pods/*"} 26 | requiredVerbs := []string{"get", "list", "create", "delete", "patch", "update", "watch", "deletecollection", "createcollection"} 27 | 28 | roleDesc, err := cs.clientset.RbacV1().Roles(workingNs).Get(context.TODO(), roleName, metav1.GetOptions{}) 29 | if err != nil { 30 | fmt.Printf("\n[%s][error] failed to get role %s in namespace %s: %v", time.Now().Format("2006-01-02 15:04:05"), roleName, workingNs, err) 31 | statusError.RBAC = fmt.Errorf("failed to get role %s in namespace %s: %v", roleName, workingNs, err) 32 | } 33 | found := false 34 | var allApiGroups, allResources, allVerbs []string 35 | for _, rule := range roleDesc.Rules { 36 | allApiGroups = append(allApiGroups, rule.APIGroups...) 37 | allResources = append(allResources, rule.Resources...) 38 | allVerbs = append(allVerbs, rule.Verbs...) 39 | } 40 | allApiGroups = dedup(allApiGroups) 41 | allResources = dedup(allResources) 42 | allVerbs = dedup(allVerbs) 43 | 44 | // fmt.Printf("Role %s in namespace %s has API Groups: %q\n", roleName, workingNs, allApiGroups) 45 | // fmt.Printf("Role %s in namespace %s has Resources: %v\n", roleName, workingNs, allResources) 46 | // fmt.Printf("Role %s in namespace %s has Verbs: %v\n", roleName, workingNs, allVerbs) 47 | 48 | if containsAll(allApiGroups, requiredApiGroup) && 49 | containsAll(allResources, requiredResources) && 50 | containsAll(allVerbs, requiredVerbs) { 51 | found = true 52 | } 53 | 54 | if found { 55 | fmt.Printf("\n[%s][INFO] Role %s in namespace %s has the required permissions", time.Now().Format("2006-01-02 15:04:05"), roleName, workingNs) 56 | } else { 57 | fmt.Printf("\n[%s][error] Role %s in namespace %s does NOT have the required permissions", time.Now().Format("2006-01-02 15:04:05"), roleName, workingNs) 58 | statusError.RBAC = fmt.Errorf("role %s in namespace %s does not have the required permissions", roleName, workingNs) 59 | } 60 | 61 | rbacBindingError := saBinding(cs) 62 | if rbacBindingError != nil { 63 | fmt.Printf("\n[%s][error] role binding error: %v", time.Now().Format("2006-01-02 15:04:05"), rbacBindingError) 64 | statusError.RBAC = fmt.Errorf("role binding error: %v", rbacBindingError) 65 | } 66 | } 67 | 68 | func saBinding(cs *ClientSet) error { 69 | workingNs := os.Getenv("WORKING_NAMESPACE") 70 | if workingNs == "" { 71 | return fmt.Errorf("WORKING_NAMESPACE environment variable is not set") 72 | } 73 | // Read the role name from the ROLE_NAME environment variable 74 | roleName := os.Getenv("ROLE_NAME") 75 | if roleName == "" { 76 | return fmt.Errorf(" environment variable is not set") 77 | } 78 | 79 | saName := os.Getenv("SERVICE_ACCOUNT_NAME") 80 | if saName == "" { 81 | return fmt.Errorf("service_account_name environment variable is not set") 82 | } 83 | 84 | roleBinding := os.Getenv("ROLE_BINDING_NAME") 85 | if roleBinding == "" { 86 | return fmt.Errorf("role_binding_name environment variable is not set") 87 | } 88 | // check if the role binding has the correct role and service account 89 | rbDesc, err := cs.clientset.RbacV1().RoleBindings(workingNs).Get(context.TODO(), roleBinding, metav1.GetOptions{}) 90 | if err != nil { 91 | return fmt.Errorf("failed to get role binding %s in namespace %s: %v", roleBinding, workingNs, err) 92 | } 93 | if rbDesc.RoleRef.Name != roleName { 94 | return fmt.Errorf("role binding %s does not reference role %s", roleBinding, roleName) 95 | } 96 | rolebindingFound := false 97 | for _, subject := range rbDesc.Subjects { 98 | if subject.Kind == "ServiceAccount" && subject.Name == saName { 99 | fmt.Printf("\n[%s][INFO] Role binding %s in namespace %s binds service account %s with role %s", time.Now().Format("2006-01-02 15:04:05"), roleBinding, workingNs, saName, roleName) 100 | rolebindingFound = true 101 | return nil 102 | } 103 | } 104 | if !rolebindingFound { 105 | return fmt.Errorf("role binding %s does not bind service account %s with role %s", roleBinding, saName, roleName) 106 | } 107 | return nil 108 | } 109 | -------------------------------------------------------------------------------- /Jenkinsfile: -------------------------------------------------------------------------------- 1 | @Library('jenkins_library') 2 | import com.blazemeter.jenkins.lib.DockerTag 3 | 4 | IMAGE_NAME = 'cranehook' 5 | 6 | pipeline { 7 | agent { 8 | docker { 9 | image 'us.gcr.io/verdant-bulwark-278/jenkins-docker-agent:node18.latest' 10 | args '--network host -u root -v /home/jenkins/tools/:/home/jenkins/tools/ -v /var/run/docker.sock:/var/run/docker.sock' 11 | label 'docker' 12 | } 13 | } 14 | 15 | parameters { 16 | string(name: 'BRANCH_NAME_PARAM', defaultValue: 'main', description: '') 17 | } 18 | 19 | environment { 20 | SENDER = 'jenkins@blazemeter.com' 21 | } 22 | 23 | stages { 24 | stage('init') { 25 | steps { 26 | script { 27 | initJenkinsGlobal() 28 | String buildDate = new Date().format("yyyyMMddHHmmss", TimeZone.getTimeZone('UTC')) 29 | env.BUILD_TIMESTAMP_ID = buildDate 30 | def branchName = env.BRANCH_NAME ?: params.BRANCH_NAME_PARAM ?: 'main' 31 | env.BRANCH_NAME = branchName 32 | env.TAG = (branchName == "main") ? "latest" : env.BRANCH_NAME 33 | 34 | echo "Set TAG=${env.TAG}" 35 | } 36 | } 37 | } 38 | 39 | stage('Install Go') { 40 | steps { 41 | sh ''' 42 | echo "Installing Go locally..." 43 | curl -sSL https://golang.org/dl/go1.21.6.linux-amd64.tar.gz -o go.tar.gz 44 | mkdir -p $WORKSPACE/go 45 | tar -C $WORKSPACE/go --strip-components=1 -xzf go.tar.gz 46 | rm go.tar.gz 47 | ''' 48 | } 49 | } 50 | 51 | stage('Build Go Binary') { 52 | steps { 53 | sh ''' 54 | echo "Building Go binary..." 55 | export GOROOT=$WORKSPACE/go 56 | export PATH=$GOROOT/bin:$PATH 57 | export GOOS=linux 58 | export GOARCH=amd64 59 | go version 60 | go build -o cranehook 61 | ''' 62 | } 63 | } 64 | 65 | stage('Build Docker Image') { 66 | steps { 67 | withCredentials([file(credentialsId: 'gcr-pull-secret-json', variable: 'GCP_KEY_FILE')]) { 68 | script { 69 | def REGISTRY = "gcr.io" 70 | def PROJECT_ID = "verdant-bulwark-278" 71 | def IMAGE_NAME = "cranehook" 72 | def VERSION_TAG = "${env.BRANCH_NAME}-${env.BUILD_NUMBER}" 73 | def LATEST_TAG = "latest" 74 | def BASE_IMAGE = "${REGISTRY}/${PROJECT_ID}/${IMAGE_NAME}" 75 | def IMAGE_WITH_VERSION = "${BASE_IMAGE}:${VERSION_TAG}" 76 | def IMAGE_LATEST = "${BASE_IMAGE}:${LATEST_TAG}" 77 | 78 | // Build the Docker image 79 | sh "docker build -t ${IMAGE_WITH_VERSION} -f Dockerfile ." 80 | sh "docker tag ${IMAGE_WITH_VERSION} ${IMAGE_LATEST}" 81 | 82 | // Authenticate to gcr.io using service account key 83 | sh ''' 84 | echo "Authenticating to gcr.io..." 85 | gcloud auth activate-service-account --key-file=$GCP_KEY_FILE 86 | gcloud auth configure-docker gcr.io --quiet 87 | ''' 88 | 89 | // Push the image 90 | sh "docker push ${IMAGE_WITH_VERSION}" 91 | sh "docker push ${IMAGE_LATEST}" 92 | 93 | echo "Pushed image: ${IMAGE_WITH_VERSION} and ${IMAGE_LATEST}" 94 | } 95 | } 96 | } 97 | } 98 | 99 | stage('Cleanup Binary') { 100 | steps { 101 | sh 'rm -f cranehook' 102 | } 103 | } 104 | 105 | stage('Perform WhiteSource scan') { 106 | when { 107 | environment name: 'BRANCH_NAME', value: 'main' 108 | } 109 | steps { 110 | script { 111 | def projectName = "${IMAGE_NAME}" 112 | whiteSourceScan("${IMAGE_NAME}", params.BRANCH_NAME_PARAM) 113 | } 114 | } 115 | } 116 | } 117 | 118 | post { 119 | cleanup { 120 | cleanWs() 121 | script { 122 | def REGISTRY = "gcr.io" 123 | def PROJECT_ID = "verdant-bulwark-278" 124 | def IMAGE_NAME = "cranehook" 125 | def VERSION_TAG = "${env.BRANCH_NAME}-${env.BUILD_NUMBER}" 126 | def LATEST_TAG = "latest" 127 | 128 | def BASE_IMAGE = "${REGISTRY}/${PROJECT_ID}/${IMAGE_NAME}" 129 | def IMAGE_WITH_VERSION = "${BASE_IMAGE}:${VERSION_TAG}" 130 | def IMAGE_LATEST = "${BASE_IMAGE}:${LATEST_TAG}" 131 | 132 | // Remove both tags locally (ignore errors if image doesn't exist) 133 | sh """ 134 | docker rmi -f ${IMAGE_WITH_VERSION} || true 135 | docker rmi -f ${IMAGE_LATEST} || true 136 | """ 137 | } 138 | } 139 | failure { 140 | // Send an email if the build fails 141 | notifyJobFailureEmailToAuthor(sender: "${env.SENDER}") 142 | } 143 | } 144 | } -------------------------------------------------------------------------------- /pkg/networkCheck.go: -------------------------------------------------------------------------------- 1 | package pkg 2 | 3 | import ( 4 | "fmt" 5 | "io" 6 | "net/http" 7 | "os" 8 | "time" 9 | ) 10 | 11 | func proxyHTTPClient() *http.Client { 12 | httpProxy := os.Getenv("HTTP_PROXY") 13 | httpsProxy := os.Getenv("HTTPS_PROXY") 14 | noProxy := os.Getenv("NO_PROXY") 15 | if httpProxy != "" || httpsProxy != "" { 16 | fmt.Printf("\n[%s][INFO] Using proxy settings: HTTP_PROXY=%s, HTTPS_PROXY=%s, NO_PROXY=%s", time.Now().Format("2006-01-02 15:04:05"), httpProxy, httpsProxy, noProxy) 17 | return &http.Client{ 18 | Transport: &http.Transport{ 19 | Proxy: http.ProxyFromEnvironment, 20 | }, 21 | } 22 | } 23 | return &http.Client{} 24 | } 25 | 26 | func (statusError *StatusError) networkCheckBlaze() { 27 | //fmt.Println("Executing networkCheckBlaze...") 28 | blazeNetworkCheck := []string{"https://a.blazemeter.com", "https://data.blazemeter.com", "https://mock.blazemeter.com", "https://auth.blazemeter.com", "https://storage.blazemeter.com", "https://bard.blazemeter.com"} 29 | client := proxyHTTPClient() 30 | for _, url := range blazeNetworkCheck { 31 | req, err := http.NewRequest("GET", url, nil) 32 | if err != nil { 33 | statement := fmt.Sprintf("This is a Go HTTP client req error for: %v", url) 34 | statusError.BlazeNetworkStatus = append(statusError.BlazeNetworkStatus, map[string]error{statement: err}) 35 | fmt.Printf("\n[%s][error] %v", time.Now().Format("2006-01-02 15:04:05"), statement) 36 | continue 37 | } 38 | resp, err := client.Do(req) 39 | if err != nil { 40 | statement := fmt.Sprintf("This is a Go HTTP client.Do error for: %v", url) 41 | statusError.BlazeNetworkStatus = append(statusError.BlazeNetworkStatus, map[string]error{statement: err}) 42 | fmt.Printf("\n[%s][error] %v", time.Now().Format("2006-01-02 15:04:05"), statement) 43 | continue 44 | } 45 | defer resp.Body.Close() 46 | if resp.StatusCode == 404 { 47 | err := fmt.Errorf("network error for %s, status code: %d", url, resp.StatusCode) 48 | statusError.BlazeNetworkStatus = append(statusError.BlazeNetworkStatus, map[string]error{url: err}) 49 | fmt.Printf("\n[%s][error] %v", time.Now().Format("2006-01-02 15:04:05"), err) 50 | body, err := io.ReadAll(resp.Body) 51 | if err != nil { 52 | fmt.Println("\nError reading response body:", err) 53 | } 54 | fmt.Println("\nResponse body:", string(body)) 55 | continue 56 | } 57 | 58 | fmt.Printf("\n[%s][INFO] Network check passed for: %s, with status %v", time.Now().Format("2006-01-02 15:04:05"), url, resp.StatusCode) 59 | } 60 | } 61 | 62 | func (statusError *StatusError) networkCheckImageRegistry() { 63 | imageRegistryCheck := os.Getenv("DOCKER_REGISTRY") 64 | imageRegistry := fmt.Sprintf("https://%s", imageRegistryCheck) 65 | 66 | client := proxyHTTPClient() 67 | req, err := http.NewRequest("GET", imageRegistry, nil) 68 | if err != nil { 69 | statusError.ImageRegistryNetworkStatus = err 70 | fmt.Printf("\n[%s][error] This is a Go HTTP client req error for: %s, %v", time.Now().Format("2006-01-02 15:04:05"), imageRegistryCheck, err) 71 | return // Exit the function if request creation fails 72 | } 73 | resp, err := client.Do(req) 74 | if err != nil { 75 | statusError.ImageRegistryNetworkStatus = err 76 | fmt.Printf("\n[%s][error] This is a Go HTTP client.Do error for: %s, %v", time.Now().Format("2006-01-02 15:04:05"), imageRegistryCheck, err) 77 | return // Exit the function if request fails 78 | } 79 | defer resp.Body.Close() 80 | if resp.StatusCode != 200 { 81 | err := fmt.Errorf("network error connecting to %s, status code: %d", imageRegistryCheck, resp.StatusCode) 82 | statusError.ImageRegistryNetworkStatus = err 83 | fmt.Printf("\n[%s][error] %v", time.Now().Format("2006-01-02 15:04:05"), err) 84 | body, err := io.ReadAll(resp.Body) 85 | if err != nil { 86 | fmt.Println("\nError reading response body:", err) 87 | } 88 | fmt.Println("\nResponse body:", string(body)) 89 | return // Exit the function if status code is not 200 90 | } 91 | fmt.Printf("\n[%s][INFO] Network check passed for: %s, with status %v", time.Now().Format("2006-01-02 15:04:05"), imageRegistryCheck, resp.StatusCode) 92 | } 93 | 94 | func (statusError *StatusError) networkCheckThirdParty() { 95 | 96 | thirdPartyNetworkCheck := []string{"https://pypi.org/", "https://storage.googleapis.com", "https://hub.docker.com", "https://index.docker.io"} 97 | client := proxyHTTPClient() 98 | for _, url := range thirdPartyNetworkCheck { 99 | req, err := http.NewRequest("GET", url, nil) 100 | if err != nil { 101 | statement := fmt.Sprintf("This is a Go HTTP client req error for: %v", url) 102 | statusError.ThirdPartyNetworkStatus = append(statusError.ThirdPartyNetworkStatus, map[string]error{statement: err}) 103 | fmt.Printf("\n[%s][error] %v", time.Now().Format("2006-01-02 15:04:05"), err) 104 | continue 105 | } 106 | resp, err := client.Do(req) 107 | if err != nil { 108 | statement := fmt.Sprintf("This is a Go HTTP client.Do error for: %v", url) 109 | statusError.ThirdPartyNetworkStatus = append(statusError.ThirdPartyNetworkStatus, map[string]error{statement: err}) 110 | fmt.Printf("\n[%s][error] %v", time.Now().Format("2006-01-02 15:04:05"), err) 111 | continue 112 | } 113 | defer resp.Body.Close() 114 | if resp.StatusCode == 404 { 115 | err := fmt.Errorf("network error, status code: %d", resp.StatusCode) 116 | statusError.ThirdPartyNetworkStatus = append(statusError.ThirdPartyNetworkStatus, map[string]error{url: err}) 117 | fmt.Printf("\n[%s][error] %v", time.Now().Format("2006-01-02 15:04:05"), err) 118 | body, err := io.ReadAll(resp.Body) 119 | if err != nil { 120 | fmt.Println("\nError reading response body:", err) 121 | } 122 | fmt.Println("\nResponse body:", string(body)) 123 | continue 124 | } 125 | fmt.Printf("\n[%s][INFO] Network check passed for: %s, with status %v", time.Now().Format("2006-01-02 15:04:05"), url, resp.StatusCode) 126 | } 127 | } 128 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Apache License 2 | Version 2.0, January 2004 3 | http://www.apache.org/licenses/ 4 | 5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 6 | 7 | 1. Definitions. 8 | 9 | "License" shall mean the terms and conditions for use, reproduction, 10 | and distribution as defined by Sections 1 through 9 of this document. 11 | 12 | "Licensor" shall mean the copyright owner or entity authorized by 13 | the copyright owner that is granting the License. 14 | 15 | "Legal Entity" shall mean the union of the acting entity and all 16 | other entities that control, are controlled by, or are under common 17 | control with that entity. For the purposes of this definition, 18 | "control" means (i) the power, direct or indirect, to cause the 19 | direction or management of such entity, whether by contract or 20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 21 | outstanding shares, or (iii) beneficial ownership of such entity. 22 | 23 | "You" (or "Your") shall mean an individual or Legal Entity 24 | exercising permissions granted by this License. 25 | 26 | "Source" form shall mean the preferred form for making modifications, 27 | including but not limited to software source code, documentation 28 | source, and configuration files. 29 | 30 | "Object" form shall mean any form resulting from mechanical 31 | transformation or translation of a Source form, including but 32 | not limited to compiled object code, generated documentation, 33 | and conversions to other media types. 34 | 35 | "Work" shall mean the work of authorship, whether in Source or 36 | Object form, made available under the License, as indicated by a 37 | copyright notice that is included in or attached to the work 38 | (an example is provided in the Appendix below). 39 | 40 | "Derivative Works" shall mean any work, whether in Source or Object 41 | form, that is based on (or derived from) the Work and for which the 42 | editorial revisions, annotations, elaborations, or other modifications 43 | represent, as a whole, an original work of authorship. For the purposes 44 | of this License, Derivative Works shall not include works that remain 45 | separable from, or merely link (or bind by name) to the interfaces of, 46 | the Work and Derivative Works thereof. 47 | 48 | "Contribution" shall mean any work of authorship, including 49 | the original version of the Work and any modifications or additions 50 | to that Work or Derivative Works thereof, that is intentionally 51 | submitted to Licensor for inclusion in the Work by the copyright owner 52 | or by an individual or Legal Entity authorized to submit on behalf of 53 | the copyright owner. For the purposes of this definition, "submitted" 54 | means any form of electronic, verbal, or written communication sent 55 | to the Licensor or its representatives, including but not limited to 56 | communication on electronic mailing lists, source code control systems, 57 | and issue tracking systems that are managed by, or on behalf of, the 58 | Licensor for the purpose of discussing and improving the Work, but 59 | excluding communication that is conspicuously marked or otherwise 60 | designated in writing by the copyright owner as "Not a Contribution." 61 | 62 | "Contributor" shall mean Licensor and any individual or Legal Entity 63 | on behalf of whom a Contribution has been received by Licensor and 64 | subsequently incorporated within the Work. 65 | 66 | 2. Grant of Copyright License. Subject to the terms and conditions of 67 | this License, each Contributor hereby grants to You a perpetual, 68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 69 | copyright license to reproduce, prepare Derivative Works of, 70 | publicly display, publicly perform, sublicense, and distribute the 71 | Work and such Derivative Works in Source or Object form. 72 | 73 | 3. Grant of Patent License. Subject to the terms and conditions of 74 | this License, each Contributor hereby grants to You a perpetual, 75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 76 | (except as stated in this section) patent license to make, have made, 77 | use, offer to sell, sell, import, and otherwise transfer the Work, 78 | where such license applies only to those patent claims licensable 79 | by such Contributor that are necessarily infringed by their 80 | Contribution(s) alone or by combination of their Contribution(s) 81 | with the Work to which such Contribution(s) was submitted. If You 82 | institute patent litigation against any entity (including a 83 | cross-claim or counterclaim in a lawsuit) alleging that the Work 84 | or a Contribution incorporated within the Work constitutes direct 85 | or contributory patent infringement, then any patent licenses 86 | granted to You under this License for that Work shall terminate 87 | as of the date such litigation is filed. 88 | 89 | 4. Redistribution. You may reproduce and distribute copies of the 90 | Work or Derivative Works thereof in any medium, with or without 91 | modifications, and in Source or Object form, provided that You 92 | meet the following conditions: 93 | 94 | (a) You must give any other recipients of the Work or 95 | Derivative Works a copy of this License; and 96 | 97 | (b) You must cause any modified files to carry prominent notices 98 | stating that You changed the files; and 99 | 100 | (c) You must retain, in the Source form of any Derivative Works 101 | that You distribute, all copyright, patent, trademark, and 102 | attribution notices from the Source form of the Work, 103 | excluding those notices that do not pertain to any part of 104 | the Derivative Works; and 105 | 106 | (d) If the Work includes a "NOTICE" text file as part of its 107 | distribution, then any Derivative Works that You distribute must 108 | include a readable copy of the attribution notices contained 109 | within such NOTICE file, excluding those notices that do not 110 | pertain to any part of the Derivative Works, in at least one 111 | of the following places: within a NOTICE text file distributed 112 | as part of the Derivative Works; within the Source form or 113 | documentation, if provided along with the Derivative Works; or, 114 | within a display generated by the Derivative Works, if and 115 | wherever such third-party notices normally appear. The contents 116 | of the NOTICE file are for informational purposes only and 117 | do not modify the License. You may add Your own attribution 118 | notices within Derivative Works that You distribute, alongside 119 | or as an addendum to the NOTICE text from the Work, provided 120 | that such additional attribution notices cannot be construed 121 | as modifying the License. 122 | 123 | You may add Your own copyright statement to Your modifications and 124 | may provide additional or different license terms and conditions 125 | for use, reproduction, or distribution of Your modifications, or 126 | for any such Derivative Works as a whole, provided Your use, 127 | reproduction, and distribution of the Work otherwise complies with 128 | the conditions stated in this License. 129 | 130 | 5. Submission of Contributions. Unless You explicitly state otherwise, 131 | any Contribution intentionally submitted for inclusion in the Work 132 | by You to the Licensor shall be under the terms and conditions of 133 | this License, without any additional terms or conditions. 134 | Notwithstanding the above, nothing herein shall supersede or modify 135 | the terms of any separate license agreement you may have executed 136 | with Licensor regarding such Contributions. 137 | 138 | 6. Trademarks. This License does not grant permission to use the trade 139 | names, trademarks, service marks, or product names of the Licensor, 140 | except as required for reasonable and customary use in describing the 141 | origin of the Work and reproducing the content of the NOTICE file. 142 | 143 | 7. Disclaimer of Warranty. Unless required by applicable law or 144 | agreed to in writing, Licensor provides the Work (and each 145 | Contributor provides its Contributions) on an "AS IS" BASIS, 146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 147 | implied, including, without limitation, any warranties or conditions 148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 149 | PARTICULAR PURPOSE. You are solely responsible for determining the 150 | appropriateness of using or redistributing the Work and assume any 151 | risks associated with Your exercise of permissions under this License. 152 | 153 | 8. Limitation of Liability. In no event and under no legal theory, 154 | whether in tort (including negligence), contract, or otherwise, 155 | unless required by applicable law (such as deliberate and grossly 156 | negligent acts) or agreed to in writing, shall any Contributor be 157 | liable to You for damages, including any direct, indirect, special, 158 | incidental, or consequential damages of any character arising as a 159 | result of this License or out of the use or inability to use the 160 | Work (including but not limited to damages for loss of goodwill, 161 | work stoppage, computer failure or malfunction, or any and all 162 | other commercial damages or losses), even if such Contributor 163 | has been advised of the possibility of such damages. 164 | 165 | 9. Accepting Warranty or Additional Liability. While redistributing 166 | the Work or Derivative Works thereof, You may choose to offer, 167 | and charge a fee for, acceptance of support, warranty, indemnity, 168 | or other liability obligations and/or rights consistent with this 169 | License. However, in accepting such obligations, You may act only 170 | on Your own behalf and on Your sole responsibility, not on behalf 171 | of any other Contributor, and only if You agree to indemnify, 172 | defend, and hold each Contributor harmless for any liability 173 | incurred by, or claims asserted against, such Contributor by reason 174 | of your accepting any such warranty or additional liability. 175 | 176 | END OF TERMS AND CONDITIONS 177 | 178 | APPENDIX: How to apply the Apache License to your work. 179 | 180 | To apply the Apache License to your work, attach the following 181 | boilerplate notice, with the fields enclosed by brackets "[]" 182 | replaced with your own identifying information. (Don't include 183 | the brackets!) The text should be enclosed in the appropriate 184 | comment syntax for the file format. We also recommend that a 185 | file or class name and description of purpose be included on the 186 | same "printed page" as the copyright notice for easier 187 | identification within third-party archives. 188 | 189 | Copyright [yyyy] [name of copyright owner] 190 | 191 | Licensed under the Apache License, Version 2.0 (the "License"); 192 | you may not use this file except in compliance with the License. 193 | You may obtain a copy of the License at 194 | 195 | http://www.apache.org/licenses/LICENSE-2.0 196 | 197 | Unless required by applicable law or agreed to in writing, software 198 | distributed under the License is distributed on an "AS IS" BASIS, 199 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 200 | See the License for the specific language governing permissions and 201 | limitations under the License. 202 | -------------------------------------------------------------------------------- /go.sum: -------------------------------------------------------------------------------- 1 | github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E= 2 | github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 3 | github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 4 | github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc h1:U9qPSI2PIWSS1VwoXQT9A3Wy9MM3WgvqSxFWenqJduM= 5 | github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 6 | github.com/emicklei/go-restful/v3 v3.11.0 h1:rAQeMHw1c7zTmncogyy8VvRZwtkmkZ4FxERmMY4rD+g= 7 | github.com/emicklei/go-restful/v3 v3.11.0/go.mod h1:6n3XBCmQQb25CM2LCACGz8ukIrRry+4bhvbpWn3mrbc= 8 | github.com/fxamacker/cbor/v2 v2.7.0 h1:iM5WgngdRBanHcxugY4JySA0nk1wZorNOpTgCMedv5E= 9 | github.com/fxamacker/cbor/v2 v2.7.0/go.mod h1:pxXPTn3joSm21Gbwsv0w9OSA2y1HFR9qXEeXQVeNoDQ= 10 | github.com/go-logr/logr v1.4.2 h1:6pFjapn8bFcIbiKo3XT4j/BhANplGihG6tvd+8rYgrY= 11 | github.com/go-logr/logr v1.4.2/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY= 12 | github.com/go-openapi/jsonpointer v0.19.6/go.mod h1:osyAmYz/mB/C3I+WsTTSgw1ONzaLJoLCyoi6/zppojs= 13 | github.com/go-openapi/jsonpointer v0.21.0 h1:YgdVicSA9vH5RiHs9TZW5oyafXZFc6+2Vc1rr/O9oNQ= 14 | github.com/go-openapi/jsonpointer v0.21.0/go.mod h1:IUyH9l/+uyhIYQ/PXVA41Rexl+kOkAPDdXEYns6fzUY= 15 | github.com/go-openapi/jsonreference v0.20.2 h1:3sVjiK66+uXK/6oQ8xgcRKcFgQ5KXa2KvnJRumpMGbE= 16 | github.com/go-openapi/jsonreference v0.20.2/go.mod h1:Bl1zwGIM8/wsvqjsOQLJ/SH+En5Ap4rVB5KVcIDZG2k= 17 | github.com/go-openapi/swag v0.22.3/go.mod h1:UzaqsxGiab7freDnrUUra0MwWfN/q7tE4j+VcZ0yl14= 18 | github.com/go-openapi/swag v0.23.0 h1:vsEVJDUo2hPJ2tu0/Xc+4noaxyEffXNIs3cOULZ+GrE= 19 | github.com/go-openapi/swag v0.23.0/go.mod h1:esZ8ITTYEsH1V2trKHjAN8Ai7xHb8RV+YSZ577vPjgQ= 20 | github.com/gogo/protobuf v1.3.2 h1:Ov1cvc58UF3b5XjBnZv7+opcTcQFZebYjWzi34vdm4Q= 21 | github.com/gogo/protobuf v1.3.2/go.mod h1:P1XiOD3dCwIKUDQYPy72D8LYyHL2YPYrpS2s69NZV8Q= 22 | github.com/golang/protobuf v1.5.4 h1:i7eJL8qZTpSEXOPTxNKhASYpMn+8e5Q6AdndVa1dWek= 23 | github.com/golang/protobuf v1.5.4/go.mod h1:lnTiLA8Wa4RWRcIUkrtSVa5nRhsEGBg48fD6rSs7xps= 24 | github.com/google/gnostic-models v0.6.8 h1:yo/ABAfM5IMRsS1VnXjTBvUb61tFIHozhlYvRgGre9I= 25 | github.com/google/gnostic-models v0.6.8/go.mod h1:5n7qKqH0f5wFt+aWF8CW6pZLLNOfYuF5OpfBSENuI8U= 26 | github.com/google/go-cmp v0.5.9/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= 27 | github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI= 28 | github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= 29 | github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= 30 | github.com/google/gofuzz v1.2.0 h1:xRy4A+RhZaiKjJ1bPfwQ8sedCA+YS2YcCHW6ec7JMi0= 31 | github.com/google/gofuzz v1.2.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= 32 | github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= 33 | github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= 34 | github.com/josharian/intern v1.0.0 h1:vlS4z54oSdjm0bgjRigI+G1HpF+tI+9rE5LLzOg8HmY= 35 | github.com/josharian/intern v1.0.0/go.mod h1:5DoeVV0s6jJacbCEi61lwdGj/aVlrQvzHFFd8Hwg//Y= 36 | github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM= 37 | github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo= 38 | github.com/kisielk/errcheck v1.5.0/go.mod h1:pFxgyoBC7bSaBwPgfKdkLd5X25qrDl4LWUI2bnpBCr8= 39 | github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck= 40 | github.com/kr/pretty v0.2.1/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI= 41 | github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= 42 | github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= 43 | github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= 44 | github.com/mailru/easyjson v0.7.7 h1:UGYAvKxe3sBsEDzO8ZeWOSlIQfWFlxbzLZe7hwFURr0= 45 | github.com/mailru/easyjson v0.7.7/go.mod h1:xzfreul335JAWq5oZzymOObrkdz5UnU4kGfJJLY9Nlc= 46 | github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= 47 | github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg= 48 | github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= 49 | github.com/modern-go/reflect2 v1.0.2 h1:xBagoLtFs94CBntxluKeaWgTMpvLxC4ur3nMaC9Gz0M= 50 | github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk= 51 | github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 h1:C3w9PqII01/Oq1c1nUAm88MOHcQC9l5mIlSMApZMrHA= 52 | github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822/go.mod h1:+n7T8mK8HuQTcFwEeznm/DIxMOiR9yIdICNftLE1DvQ= 53 | github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= 54 | github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= 55 | github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= 56 | github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= 57 | github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw= 58 | github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo= 59 | github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= 60 | github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= 61 | github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= 62 | github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4= 63 | github.com/x448/float16 v0.8.4 h1:qLwI1I70+NjRFUR3zs1JPUCgaCXSh3SW62uAKT1mSBM= 64 | github.com/x448/float16 v0.8.4/go.mod h1:14CWIYCyZA/cWjXOioeEpHeN/83MdbZDRQHoFcYsOfg= 65 | github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= 66 | github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= 67 | golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= 68 | golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= 69 | golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= 70 | golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= 71 | golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= 72 | golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= 73 | golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= 74 | golang.org/x/net v0.0.0-20200226121028-0de0cce0169b/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= 75 | golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= 76 | golang.org/x/net v0.30.0 h1:AcW1SDZMkb8IpzCdQUaIq2sP4sZ4zw+55h6ynffypl4= 77 | golang.org/x/net v0.30.0/go.mod h1:2wGyMJ5iFasEhkwi13ChkO/t1ECNC4X4eBKkVFyYFlU= 78 | golang.org/x/oauth2 v0.23.0 h1:PbgcYx2W7i4LvjJWEbf0ngHV6qJYr86PkAV3bXdLEbs= 79 | golang.org/x/oauth2 v0.23.0/go.mod h1:XYTD2NtWslqkgxebSiOHnXEap4TF09sJSc7H1sXbhtI= 80 | golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 81 | golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 82 | golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 83 | golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= 84 | golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 85 | golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 86 | golang.org/x/sys v0.26.0 h1:KHjCJyddX0LoSTb3J+vWpupP9p0oznkqVk/IfjymZbo= 87 | golang.org/x/sys v0.26.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= 88 | golang.org/x/term v0.25.0 h1:WtHI/ltw4NvSUig5KARz9h521QvRC8RmF/cuYqifU24= 89 | golang.org/x/term v0.25.0/go.mod h1:RPyXicDX+6vLxogjjRxjgD2TKtmAO6NZBsBRfrOLu7M= 90 | golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= 91 | golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= 92 | golang.org/x/text v0.19.0 h1:kTxAhCbGbxhK0IwgSKiMO5awPoDQ0RpfiVYBfK860YM= 93 | golang.org/x/text v0.19.0/go.mod h1:BuEKDfySbSR4drPmRPG/7iBdf8hvFMuRexcpahXilzY= 94 | golang.org/x/time v0.7.0 h1:ntUhktv3OPE6TgYxXWv9vKvUSJyIFJlyohwbkEwPrKQ= 95 | golang.org/x/time v0.7.0/go.mod h1:3BpzKBy/shNhVucY/MWOyx10tF3SFh9QdLuxbVysPQM= 96 | golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= 97 | golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= 98 | golang.org/x/tools v0.0.0-20200619180055-7c47624df98f/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= 99 | golang.org/x/tools v0.0.0-20210106214847-113979e3529a/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= 100 | golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= 101 | golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= 102 | golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= 103 | golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= 104 | google.golang.org/protobuf v1.35.1 h1:m3LfL6/Ca+fqnjnlqQXNpFPABW1UD7mjh8KO2mKFytA= 105 | google.golang.org/protobuf v1.35.1/go.mod h1:9fA7Ob0pmnwhb644+1+CVWFRbNajQ6iRojtC/QF5bRE= 106 | gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 107 | gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q= 108 | gopkg.in/evanphx/json-patch.v4 v4.12.0 h1:n6jtcsulIzXPJaxegRbvFNNrZDjbij7ny3gmSPG+6V4= 109 | gopkg.in/evanphx/json-patch.v4 v4.12.0/go.mod h1:p8EYWUEYMpynmqDbY58zCKCFZw8pRWMG4EsWvDvM72M= 110 | gopkg.in/inf.v0 v0.9.1 h1:73M5CoZyi3ZLMOyDlQh031Cx6N9NDJ2Vvfl76EDAgDc= 111 | gopkg.in/inf.v0 v0.9.1/go.mod h1:cWUDdTG/fYaXco+Dcufb5Vnc6Gp2YChqWtbxRZE0mXw= 112 | gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= 113 | gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= 114 | gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= 115 | k8s.io/api v0.32.2 h1:bZrMLEkgizC24G9eViHGOPbW+aRo9duEISRIJKfdJuw= 116 | k8s.io/api v0.32.2/go.mod h1:hKlhk4x1sJyYnHENsrdCWw31FEmCijNGPJO5WzHiJ6Y= 117 | k8s.io/apimachinery v0.32.2 h1:yoQBR9ZGkA6Rgmhbp/yuT9/g+4lxtsGYwW6dR6BDPLQ= 118 | k8s.io/apimachinery v0.32.2/go.mod h1:GpHVgxoKlTxClKcteaeuF1Ul/lDVb74KpZcxcmLDElE= 119 | k8s.io/client-go v0.32.2 h1:4dYCD4Nz+9RApM2b/3BtVvBHw54QjMFUl1OLcJG5yOA= 120 | k8s.io/client-go v0.32.2/go.mod h1:fpZ4oJXclZ3r2nDOv+Ux3XcJutfrwjKTCHz2H3sww94= 121 | k8s.io/klog/v2 v2.130.1 h1:n9Xl7H1Xvksem4KFG4PYbdQCQxqc/tTUyrgXaOhHSzk= 122 | k8s.io/klog/v2 v2.130.1/go.mod h1:3Jpz1GvMt720eyJH1ckRHK1EDfpxISzJ7I9OYgaDtPE= 123 | k8s.io/kube-openapi v0.0.0-20241105132330-32ad38e42d3f h1:GA7//TjRY9yWGy1poLzYYJJ4JRdzg3+O6e8I+e+8T5Y= 124 | k8s.io/kube-openapi v0.0.0-20241105132330-32ad38e42d3f/go.mod h1:R/HEjbvWI0qdfb8viZUeVZm0X6IZnxAydC7YU42CMw4= 125 | k8s.io/utils v0.0.0-20241104100929-3ea5e8cea738 h1:M3sRQVHv7vB20Xc2ybTt7ODCeFj6JSWYFzOFnYeS6Ro= 126 | k8s.io/utils v0.0.0-20241104100929-3ea5e8cea738/go.mod h1:OLgZIPagt7ERELqWJFomSt595RzquPNLL48iOWgYOg0= 127 | sigs.k8s.io/json v0.0.0-20241010143419-9aa6b5e7a4b3 h1:/Rv+M11QRah1itp8VhT6HoVx1Ray9eB4DBr+K+/sCJ8= 128 | sigs.k8s.io/json v0.0.0-20241010143419-9aa6b5e7a4b3/go.mod h1:18nIHnGi6636UCz6m8i4DhaJ65T6EruyzmoQqI2BVDo= 129 | sigs.k8s.io/structured-merge-diff/v4 v4.4.2 h1:MdmvkGuXi/8io6ixD5wud3vOLwc1rj0aNqRlpuvjmwA= 130 | sigs.k8s.io/structured-merge-diff/v4 v4.4.2/go.mod h1:N8f93tFZh9U6vpxwRArLiikrE5/2tiu1w1AGfACIGE4= 131 | sigs.k8s.io/yaml v1.4.0 h1:Mk1wCc2gy/F0THH0TAp1QYyJNzRm2KCLy3o5ASXVI5E= 132 | sigs.k8s.io/yaml v1.4.0/go.mod h1:Ejl7/uTz7PSA4eKMyQCUTnhZYNmLIl+5c2lQPGR2BPY= 133 | -------------------------------------------------------------------------------- /pkg/ingressCheck.go: -------------------------------------------------------------------------------- 1 | package pkg 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | "os" 7 | "time" 8 | 9 | "k8s.io/client-go/dynamic" 10 | "k8s.io/client-go/rest" 11 | 12 | metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" 13 | "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" 14 | "k8s.io/apimachinery/pkg/runtime/schema" 15 | "k8s.io/client-go/kubernetes" 16 | ) 17 | 18 | func (statusError *StatusError) checkIngress(cs *ClientSet) { 19 | clientset := cs.clientset 20 | ingressType := os.Getenv("KUBERNETES_WEB_EXPOSE_TYPE") 21 | var ingressNs string 22 | switch ingressType != "" { 23 | case ingressType == "ISTIO": 24 | ingressNs = "istio-system" 25 | case ingressType == "INGRESS": 26 | ingressNs = "ingress-nginx" 27 | default: 28 | fmt.Printf("\n[%s][error] kubernetes_web_expose_type environment variable is not set or has an invalid value", time.Now().Format("2006-01-02 15:04:05")) 29 | statusError.IngressStatus = fmt.Errorf("kubernetes_web_expose_type environment variable is not set or has an invalid value") 30 | return // Exit the function if the ingress type is not set or invalid 31 | } 32 | 33 | listNamespaces, err := clientset.CoreV1().Namespaces().List(context.TODO(), metav1.ListOptions{}) 34 | if err != nil { 35 | fmt.Printf("\n[%s][error] listing namespaces: %v", time.Now().Format("2006-01-02 15:04:05"), err) 36 | statusError.IngressStatus = fmt.Errorf("[%s] error listing namespaces: %v", time.Now().Format("2006-01-02 15:04:05"), err) 37 | } else { 38 | 39 | nsfound := false 40 | for _, names := range listNamespaces.Items { 41 | if ingressNs == names.Name { 42 | fmt.Printf("\n[%s][INFO] Namespace %s exists for the web service type %s", time.Now().Format("2006-01-02 15:04:05"), names.Name, ingressType) 43 | nsfound = true 44 | break // Exit the loop as we found the matching namespace 45 | } 46 | continue 47 | } 48 | 49 | if !nsfound { 50 | fmt.Printf("\n[%s][error] namespace %s does not exist for the web service type %s", time.Now().Format("2006-01-02 15:04:05"), ingressNs, ingressType) 51 | statusError.IngressStatus = fmt.Errorf("[%s] namespace %s does not exist for the web service type %s", time.Now().Format("2006-01-02 15:04:05"), ingressNs, ingressType) 52 | } 53 | if nsfound { 54 | err := checkIngressResouces(ingressNs, clientset) 55 | if err != nil { 56 | fmt.Printf("\n[%s][error] checking ingress resources: %v", time.Now().Format("2006-01-02 15:04:05"), err) 57 | statusError.IngressStatus = fmt.Errorf("[%s] error checking ingress resources: %v", time.Now().Format("2006-01-02 15:04:05"), err) 58 | } 59 | err = checkSecret(clientset) 60 | if err != nil { 61 | fmt.Printf("\n[%s][error] checking ingress secret: %v", time.Now().Format("2006-01-02 15:04:05"), err) 62 | statusError.IngressStatus = fmt.Errorf("[%s] error checking ingress secret: %v", time.Now().Format("2006-01-02 15:04:05"), err) 63 | } 64 | 65 | if ingressType == "ISTIO" { 66 | roleErr := ingressRoleCheckIstio(clientset) 67 | if roleErr != nil { 68 | fmt.Printf("\n[%s][error] checking istio-system role: %v", time.Now().Format("2006-01-02 15:04:05"), roleErr) 69 | statusError.IngressStatus = fmt.Errorf("[%s] error checking istio-system role: %v", time.Now().Format("2006-01-02 15:04:05"), roleErr) 70 | } 71 | labelErr := labelCheckIstio(clientset) 72 | if labelErr != nil { 73 | fmt.Printf("\n[%s][error] checking istio ingress labels: %v", time.Now().Format("2006-01-02 15:04:05"), labelErr) 74 | statusError.IngressStatus = fmt.Errorf("[%s] error checking istio ingress labels: %v", time.Now().Format("2006-01-02 15:04:05"), labelErr) 75 | } 76 | gatewayErr := gatewayCheck() 77 | if gatewayErr != nil { 78 | fmt.Printf("\n[%s][error] checking ingress gateway: %v", time.Now().Format("2006-01-02 15:04:05"), gatewayErr) 79 | statusError.IngressStatus = fmt.Errorf("[%s] error checking ingress gateway: %v", time.Now().Format("2006-01-02 15:04:05"), gatewayErr) 80 | } 81 | } 82 | if ingressType == "INGRESS" { 83 | err := ingressRoleCheckNginx(clientset) 84 | if err != nil { 85 | fmt.Printf("\n[%s][error] checking nginx-ingress role: %v", time.Now().Format("2006-01-02 15:04:05"), err) 86 | statusError.IngressStatus = fmt.Errorf("[%s] error checking nginx-ingress role: %v", time.Now().Format("2006-01-02 15:04:05"), err) 87 | } 88 | } 89 | } 90 | } 91 | } 92 | 93 | func checkIngressResouces(ingressNs string, clientset *kubernetes.Clientset) error { 94 | svcList, err := clientset.CoreV1().Services(ingressNs).List(context.TODO(), metav1.ListOptions{}) 95 | if err != nil { 96 | return err 97 | } 98 | 99 | lbfound := false 100 | externalIpfound := false 101 | for _, svcType := range svcList.Items { 102 | if svcType.Spec.Type == "LoadBalancer" { 103 | fmt.Printf("\n[%s][INFO] Service %s is of type LoadBalancer", time.Now().Format("2006-01-02 15:04:05"), svcType.Name) 104 | lbfound = true 105 | // Check if the service has an external IP 106 | if len(svcType.Status.LoadBalancer.Ingress) > 0 { 107 | fmt.Printf("\n[%s][INFO] Service %s has an external IP: %s", time.Now().Format("2006-01-02 15:04:05"), svcType.Name, svcType.Status.LoadBalancer.Ingress[0].IP) 108 | externalIpfound = true 109 | } 110 | } 111 | } 112 | 113 | if !lbfound { 114 | return fmt.Errorf("\n[%s] loadbalancer service type not found in the namespace %s", time.Now().Format("2006-01-02 15:04:05"), ingressNs) 115 | } 116 | if !externalIpfound { 117 | return fmt.Errorf("\n[%s] external ip not found for the loadbalancer service type in the namespace %s", time.Now().Format("2006-01-02 15:04:05"), ingressNs) 118 | } 119 | return nil 120 | } 121 | 122 | func ingressRoleCheckNginx(clientset *kubernetes.Clientset) error { 123 | // Read the namespace from the WORKING_NAMESPACE environment variable 124 | workingNs := os.Getenv("WORKING_NAMESPACE") 125 | if workingNs == "" { 126 | return fmt.Errorf("working_namespace environment variable is not set") 127 | } 128 | 129 | // Read the role name from the ROLE_NAME environment variable 130 | roleName := os.Getenv("ROLE_NAME") 131 | if roleName == "" { 132 | return fmt.Errorf("role_name environment variable is not set") 133 | } 134 | 135 | // Fetch the specific role by name 136 | role, err := clientset.RbacV1().Roles(workingNs).Get(context.TODO(), roleName, metav1.GetOptions{}) 137 | if err != nil { 138 | return fmt.Errorf("failed to get role %s in namespace %s: %v", roleName, workingNs, err) 139 | } 140 | 141 | // Define the required rules 142 | requiredApiGroup := "networking.k8s.io" 143 | requiredResources := []string{"virtualservices", "gateways", "ingresses"} 144 | requiredVerbs := []string{"get", "list", "create", "delete", "patch", "update"} 145 | 146 | // Check the role's rules for the required permissions 147 | found := false 148 | for _, rule := range role.Rules { 149 | if contains(rule.APIGroups, requiredApiGroup) && 150 | containsAll(rule.Resources, requiredResources) && 151 | containsAll(rule.Verbs, requiredVerbs) { 152 | fmt.Printf("\n[%s][INFO] Role %s in namespace %s has the required permissions to run SV with Nginx", time.Now().Format("2006-01-02 15:04:05"), roleName, workingNs) 153 | found = true 154 | break 155 | } 156 | } 157 | if !found { 158 | //fmt.Printf("Role %s in namespace %s does NOT have the required permissions to run SV with Nginx. Rules:\n", roleName, workingNs) 159 | return fmt.Errorf("role %s in namespace %s does not have the required permissions to run sv with nginx", roleName, workingNs) 160 | } 161 | return nil 162 | 163 | } 164 | 165 | func ingressRoleCheckIstio(clientset *kubernetes.Clientset) error { 166 | // Read the namespace from the WORKING_NAMESPACE environment variable 167 | workingNs := os.Getenv("WORKING_NAMESPACE") 168 | if workingNs == "" { 169 | return fmt.Errorf("working_namespace environment variable is not set") 170 | } 171 | 172 | // Read the role name from the ROLE_NAME environment variable 173 | roleName := os.Getenv("ROLE_NAME") 174 | if roleName == "" { 175 | return fmt.Errorf("role_name environment variable is not set") 176 | } 177 | 178 | // Fetch the specific role by name 179 | role, err := clientset.RbacV1().Roles(workingNs).Get(context.TODO(), roleName, metav1.GetOptions{}) 180 | if err != nil { 181 | return fmt.Errorf("failed to get role %s in namespace %s: %v", roleName, workingNs, err) 182 | } 183 | 184 | // Define the required rules 185 | requiredApiGroup := "networking.istio.io" 186 | requiredResources := []string{"destinationrules", "virtualservices", "gateways"} 187 | requiredVerbs := []string{"get", "list", "create", "delete", "patch", "update"} 188 | 189 | // Check the role's rules for the required permissions 190 | found := false 191 | for _, rule := range role.Rules { 192 | if contains(rule.APIGroups, requiredApiGroup) && 193 | containsAll(rule.Resources, requiredResources) && 194 | containsAll(rule.Verbs, requiredVerbs) { 195 | fmt.Printf("\n[%s][INFO] Role %s in namespace %s has the required permissions to run SV with Istio", time.Now().Format("2006-01-02 15:04:05"), roleName, workingNs) 196 | found = true 197 | break 198 | } 199 | } 200 | if !found { 201 | //fmt.Printf("Role %s in namespace %s does NOT have the required permissions to run SV with Istio. Rules:\n", roleName, workingNs) 202 | return fmt.Errorf("role %s in namespace %s does not have the required permissions to run sv with istio", roleName, workingNs) 203 | } 204 | return nil 205 | } 206 | 207 | func gatewayCheck() error { 208 | workingNs := os.Getenv("WORKING_NAMESPACE") 209 | // Create Kubernetes REST configuration 210 | config, err := rest.InClusterConfig() 211 | if err != nil { 212 | return fmt.Errorf("failed to create Kubernetes rest config: %v", err) 213 | } 214 | // Create a dynamic client 215 | dynamicClient, err := dynamic.NewForConfig(config) 216 | if err != nil { 217 | return fmt.Errorf("failed to create dynamic client: %v", err) 218 | } 219 | 220 | // Read the gateway name from the environment variable 221 | gatewayName := os.Getenv("KUBERNETES_ISTIO_GATEWAY_NAME") 222 | if gatewayName == "" { 223 | return fmt.Errorf("kubernetes_web_expose_type environment variable is not set") 224 | } 225 | 226 | // Fetch the Gateway resource from the Istio API group 227 | gateway, err := dynamicClient.Resource( 228 | schema.GroupVersionResource{ 229 | Group: "networking.istio.io", 230 | Version: "v1beta1", 231 | Resource: "gateways", 232 | }, 233 | ).Namespace(workingNs).Get(context.TODO(), gatewayName, metav1.GetOptions{}) 234 | if err != nil { 235 | return fmt.Errorf("failed to get Gateway %s in namespace %s: %v", gatewayName, workingNs, err) 236 | } 237 | 238 | // Extract the spec from the Gateway resource 239 | spec, found, err := unstructured.NestedMap(gateway.Object, "spec") 240 | if err != nil || !found { 241 | return fmt.Errorf("failed to extract spec from Gateway %s: %v", gatewayName, err) 242 | } 243 | 244 | // Validate the spec fields 245 | if err := validateGatewaySpec(spec); err != nil { 246 | fmt.Printf("gateway %s spec validation failed: %v", gatewayName, err) 247 | return fmt.Errorf("gateway %s spec validation failed: %v", gatewayName, err) 248 | } 249 | 250 | fmt.Printf("\n[%s][INFO] Gateway %s in namespace %s matches the required spec", time.Now().Format("2006-01-02 15:04:05"), gatewayName, workingNs) 251 | return nil 252 | } 253 | 254 | func validateGatewaySpec(spec map[string]interface{}) error { 255 | wildcardCredential := os.Getenv("KUBERNETES_WEB_EXPOSE_TLS_SECRET_NAME") 256 | 257 | selector, ok := spec["selector"].(map[string]interface{}) 258 | if !ok || selector["istio"] != "ingressgateway" { 259 | return fmt.Errorf("selector.istio must be 'ingressgateway'") 260 | } 261 | 262 | servers, ok := spec["servers"].([]interface{}) 263 | if !ok || len(servers) < 3 { 264 | return fmt.Errorf("spec.servers must contain at least 3 entries") 265 | } 266 | 267 | for _, s := range servers { 268 | server, ok := s.(map[string]interface{}) 269 | if !ok { 270 | return fmt.Errorf("invalid server entry in spec.servers") 271 | } 272 | port, ok := server["port"].(map[string]interface{}) 273 | if !ok { 274 | return fmt.Errorf("port is not a map[string]interface{}") 275 | } 276 | number, err := getPortNumber(port["number"]) 277 | if err != nil { 278 | return err 279 | } 280 | protocol, ok := port["protocol"].(string) 281 | if !ok { 282 | return fmt.Errorf("protocol is not a string") 283 | } 284 | 285 | switch number { 286 | case 80: 287 | if protocol != "HTTP" { 288 | return fmt.Errorf("port 80 must use protocol http") 289 | } 290 | case 443: 291 | if protocol != "HTTPS" { 292 | return fmt.Errorf("port 443 must use protocol https") 293 | } 294 | tls, err := getTLS(server) 295 | if err != nil { 296 | return fmt.Errorf("port 443: %v", err) 297 | } 298 | if tls["mode"] != "SIMPLE" { 299 | return fmt.Errorf("port 443 must use tls mode simple") 300 | } 301 | if tls["credentialName"] != wildcardCredential { 302 | return fmt.Errorf("port 443 must use the wildcard credential named %s", wildcardCredential) 303 | } 304 | case 15443: 305 | if protocol != "HTTPS" { 306 | return fmt.Errorf("port 15443 must use protocol https") 307 | } 308 | tls, err := getTLS(server) 309 | if err != nil { 310 | return fmt.Errorf("port 15443: %v", err) 311 | } 312 | if tls["mode"] != "PASSTHROUGH" { 313 | return fmt.Errorf("port 15443 must use tls mode passthrough") 314 | } 315 | } 316 | } 317 | return nil 318 | } 319 | 320 | // getPortNumber safely extracts the port number as int, otherwise returns an error 321 | func getPortNumber(val interface{}) (int, error) { 322 | switch n := val.(type) { 323 | case int: 324 | return n, nil 325 | case int64: 326 | return int(n), nil 327 | case float64: 328 | return int(n), nil 329 | default: 330 | return 0, fmt.Errorf("port number is not a valid number type") 331 | } 332 | } 333 | 334 | // getTLS safely extracts the tls map from a server entry 335 | func getTLS(server map[string]interface{}) (map[string]interface{}, error) { 336 | tlsVal, ok := server["tls"] 337 | if !ok || tlsVal == nil { 338 | return nil, fmt.Errorf("must have tls configuration") 339 | } 340 | tls, ok := tlsVal.(map[string]interface{}) 341 | if !ok { 342 | return nil, fmt.Errorf("tls is not a map[string]interface{}") 343 | } 344 | return tls, nil 345 | } 346 | 347 | func labelCheckIstio(clientset *kubernetes.Clientset) error { 348 | workingNs := os.Getenv("WORKING_NAMESPACE") 349 | nsObject, error := clientset.CoreV1().Namespaces().Get(context.TODO(), workingNs, metav1.GetOptions{}) 350 | if error != nil { 351 | return fmt.Errorf("failed to get namespace %s to check labels: %v", workingNs, error) 352 | } 353 | istioInjection := false 354 | getLabels := nsObject.GetLabels() 355 | for key, value := range getLabels { 356 | if key == "istio-injection" && value == "enabled" { 357 | fmt.Printf("\n[%s][INFO] Namespace %s has the istio-injection label set to enabled", time.Now().Format("2006-01-02 15:04:05"), workingNs) 358 | istioInjection = true 359 | return nil 360 | } 361 | continue 362 | } 363 | if !istioInjection { 364 | return fmt.Errorf("namespace %s does not have the istio-injection label set to enabled", workingNs) 365 | } 366 | return nil 367 | } 368 | 369 | func checkSecret(clientset *kubernetes.Clientset) error { 370 | // Read the namespace from the WORKING_NAMESPACE environment variable 371 | ingressType := os.Getenv("KUBERNETES_WEB_EXPOSE_TYPE") 372 | if ingressType == "" { 373 | return fmt.Errorf("kubernetes_web_expose_type environment variable is not set") 374 | } 375 | workingNs := os.Getenv("WORKING_NAMESPACE") 376 | if workingNs == "" { 377 | return fmt.Errorf("working_namespace environment variable is not set") 378 | } 379 | 380 | // Read the secret name from the SECRET_NAME environment variable 381 | secretName := os.Getenv("KUBERNETES_WEB_EXPOSE_TLS_SECRET_NAME") 382 | if secretName == "" { 383 | return fmt.Errorf("kubernetes_web_expose_tls_secret_name environment variable is not set") 384 | } 385 | if ingressType == "ISTIO" { 386 | secret, err := clientset.CoreV1().Secrets("istio-system").Get(context.TODO(), secretName, metav1.GetOptions{}) 387 | if err != nil { 388 | return fmt.Errorf("failed to get secret %s in namespace instio-ingresss: %v", secretName, err) 389 | } 390 | fmt.Printf("\n[%s][INFO] Secret %s is found (%s) in namespace istio-system", time.Now().Format("2006-01-02 15:04:05"), secretName, secret.Name) 391 | } 392 | if ingressType == "INGRESS" { 393 | // Fetch the specific secret by name 394 | secret, err := clientset.CoreV1().Secrets(workingNs).Get(context.TODO(), secretName, metav1.GetOptions{}) 395 | if err != nil { 396 | return fmt.Errorf("failed to get secret %s in namespace %s: %v", secretName, workingNs, err) 397 | } 398 | fmt.Printf("\n[%s][INFO] Secret %s is found (%s) in namespace %s", time.Now().Format("2006-01-02 15:04:05"), secretName, secret.Name, workingNs) 399 | } 400 | return nil 401 | } 402 | --------------------------------------------------------------------------------