From cf2a8b9b9fbb021d7858a2ace62649af4451f14c Mon Sep 17 00:00:00 2001 From: Ben Jackson Date: Sat, 2 Dec 2023 15:42:31 +1100 Subject: [PATCH] feat: initial pass at service cleanup --- cmd/identify_ingress_test.go | 4 + cmd/identify_services.go | 70 +++++++++++++++++ cmd/identify_services_test.go | 103 ++++++++++++++++++++++++++ internal/helpers/helpers.go | 9 +++ legacy/build-deploy-docker-compose.sh | 78 +++++++++++++++++++ 5 files changed, 264 insertions(+) create mode 100644 cmd/identify_services.go create mode 100644 cmd/identify_services_test.go diff --git a/cmd/identify_ingress_test.go b/cmd/identify_ingress_test.go index ba83a600..e76010b8 100644 --- a/cmd/identify_ingress_test.go +++ b/cmd/identify_ingress_test.go @@ -5,6 +5,7 @@ import ( "reflect" "testing" + "github.com/uselagoon/build-deploy-tool/internal/helpers" "github.com/uselagoon/build-deploy-tool/internal/lagoon" "github.com/uselagoon/build-deploy-tool/internal/testdata" ) @@ -378,6 +379,9 @@ func TestIdentifyRoute(t *testing.T) { if string(retJSON) != tt.wantJSON { t.Errorf("returned autogen %v doesn't match want %v", string(retJSON), tt.wantJSON) } + t.Cleanup(func() { + helpers.UnsetEnvVars(nil) + }) }) } } diff --git a/cmd/identify_services.go b/cmd/identify_services.go new file mode 100644 index 00000000..63f5b09d --- /dev/null +++ b/cmd/identify_services.go @@ -0,0 +1,70 @@ +package cmd + +import ( + "encoding/json" + "fmt" + + "github.com/spf13/cobra" + generator "github.com/uselagoon/build-deploy-tool/internal/generator" + "github.com/uselagoon/build-deploy-tool/internal/helpers" +) + +type identifyServices struct { + Name string `json:"name"` + Type string `json:"type"` +} + +var servicesIdentify = &cobra.Command{ + Use: "services", + Aliases: []string{"s"}, + Short: "Identify services that this build would create", + RunE: func(cmd *cobra.Command, args []string) error { + generator, err := generator.GenerateInput(*rootCmd, false) + if err != nil { + return err + } + ret, _, err := IdentifyServices(generator) + if err != nil { + return err + } + retJSON, _ := json.Marshal(ret) + fmt.Println(string(retJSON)) + return nil + }, +} + +// IdentifyServices identifies services that this build would create +func IdentifyServices(g generator.GeneratorInput) ([]string, []identifyServices, error) { + lagoonBuild, err := generator.NewGenerator( + g, + ) + if err != nil { + return nil, nil, err + } + + services := []string{} + serviceTypes := []identifyServices{} + for _, service := range lagoonBuild.BuildValues.Services { + if service.Type != "" { + services = helpers.AppendIfMissing(services, service.OverrideName) + serviceTypes = AppendIfMissing(serviceTypes, identifyServices{ + Name: service.OverrideName, + Type: service.Type, + }) + } + } + return services, serviceTypes, nil +} + +func init() { + identifyCmd.AddCommand(servicesIdentify) +} + +func AppendIfMissing(slice []identifyServices, i identifyServices) []identifyServices { + for _, ele := range slice { + if ele.Name == i.Name { + return slice + } + } + return append(slice, i) +} diff --git a/cmd/identify_services_test.go b/cmd/identify_services_test.go new file mode 100644 index 00000000..65c7ea7f --- /dev/null +++ b/cmd/identify_services_test.go @@ -0,0 +1,103 @@ +package cmd + +import ( + "reflect" + "testing" + + "github.com/uselagoon/build-deploy-tool/internal/helpers" + "github.com/uselagoon/build-deploy-tool/internal/lagoon" + "github.com/uselagoon/build-deploy-tool/internal/testdata" +) + +func TestIdentifyServices(t *testing.T) { + tests := []struct { + name string + args testdata.TestData + templatePath string + want []string + wantServices []identifyServices + wantErr bool + }{ + { + name: "test1 single service", + args: testdata.GetSeedData( + testdata.TestData{ + ProjectName: "example-project", + EnvironmentName: "main", + Branch: "main", + LagoonYAML: "../internal/testdata/node/lagoon.yml", + }, true), + templatePath: "testdata/output", + want: []string{"node"}, + wantServices: []identifyServices{{Name: "node", Type: "node"}}, + }, + { + name: "test2 complex servives", + args: testdata.GetSeedData( + testdata.TestData{ + ProjectName: "example-project", + EnvironmentName: "main", + Branch: "main", + EnvironmentType: "development", + LagoonYAML: "../internal/testdata/complex/lagoon.yml", + }, true), + templatePath: "testdata/output", + want: []string{"cli", "nginx-php", "mariadb", "redis"}, + wantServices: []identifyServices{ + {Name: "cli", Type: "cli-persistent"}, + {Name: "nginx-php", Type: "nginx-php-persistent"}, + {Name: "mariadb", Type: "mariadb-dbaas"}, + {Name: "redis", Type: "redis"}, + }, + }, + { + name: "test3 complex servives where one is removed", + args: testdata.GetSeedData( + testdata.TestData{ + ProjectName: "example-project", + EnvironmentName: "main", + Branch: "main", + EnvironmentType: "development", + LagoonYAML: "../internal/testdata/complex/lagoon.yml", + EnvVariables: []lagoon.EnvironmentVariable{ + { + Name: "LAGOON_SERVICE_TYPES", + Value: "redis:none", + Scope: "build", + }, + }, + }, true), + templatePath: "testdata/output", + want: []string{"cli", "nginx-php", "mariadb"}, + wantServices: []identifyServices{ + {Name: "cli", Type: "cli-persistent"}, + {Name: "nginx-php", Type: "nginx-php-persistent"}, + {Name: "mariadb", Type: "mariadb-dbaas"}, + }, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + // set the environment variables from args + savedTemplates := tt.templatePath + generator, err := testdata.SetupEnvironment(*rootCmd, savedTemplates, tt.args) + if err != nil { + t.Errorf("%v", err) + } + got, got2, err := IdentifyServices(generator) + if (err != nil) != tt.wantErr { + t.Errorf("IdentifyServices() error = %v, wantErr %v", err, tt.wantErr) + return + } + if !reflect.DeepEqual(got, tt.want) { + t.Errorf("IdentifyServices() = %v, want %v", got, tt.want) + } + if !reflect.DeepEqual(got2, tt.wantServices) { + t.Errorf("IdentifyServices() = %v, want %v", got2, tt.wantServices) + } + t.Cleanup(func() { + helpers.UnsetEnvVars(nil) + }) + }) + } +} diff --git a/internal/helpers/helpers.go b/internal/helpers/helpers.go index 1b979c66..8d7e3f43 100644 --- a/internal/helpers/helpers.go +++ b/internal/helpers/helpers.go @@ -236,3 +236,12 @@ func DeepCopy(src, dist interface{}) (err error) { } return gob.NewDecoder(&buf).Decode(dist) } + +func AppendIfMissing(slice []string, i string) []string { + for _, ele := range slice { + if ele == i { + return slice + } + } + return append(slice, i) +} diff --git a/legacy/build-deploy-docker-compose.sh b/legacy/build-deploy-docker-compose.sh index 2a66298d..2a2731ac 100755 --- a/legacy/build-deploy-docker-compose.sh +++ b/legacy/build-deploy-docker-compose.sh @@ -1832,6 +1832,84 @@ set +x currentStepEnd="$(date +"%Y-%m-%d %H:%M:%S")" patchBuildStep "${buildStartTime}" "${previousStepEnd}" "${currentStepEnd}" "${NAMESPACE}" "deploymentApplyComplete" "Applying Deployments" "false" previousStepEnd=${currentStepEnd} +beginBuildStep "Service/Deployment Cleanup" "cleanupServices" + +############################################## +### CLEANUP services which have been removed from docker-compose.yaml +##############################################s + +set +x +# collect the current routes, its possible to exclude ingress by adding a label 'route.lagoon.sh/remove=false' and it won't get deleted +CURRENT_SERVICES=$(kubectl -n ${NAMESPACE} get deployments -l "lagoon.sh/service-type" -l "lagoon.sh/service" --no-headers | cut -d " " -f 1 | xargs) +# collect the routes that Lagoon thinks it should have based on the .lagoon.yml and any routes that have come from the api +# using the build-deploy-tool generator +SERVICES_TO_JSON=$(build-deploy-tool identify services | jq -r '.[]') + +MATCHED_SERVICE=false +DELETE_SERVICE=() +# loop over the routes from kubernetes +for EXIST_SERVICE in ${CURRENT_SERVICES}; do + # loop over the routes that Lagoon thinks it should have + for SERVICE in ${SERVICES_TO_JSON}; do + if [ "${EXIST_SERVICE}" == "${SERVICE}" ]; then + MATCHED_SERVICE=true + continue + fi + done + if [ "${MATCHED_SERVICE}" != "true" ]; then + DELETE_SERVICE+=($EXIST_SERVICE) + fi + MATCHED_SERVICE=false +done + +SERVICE_CLEANUP_WARNINGS="false" +if [ ${#DELETE_SERVICE[@]} -ne 0 ]; then + SERVICE_CLEANUP_WARNINGS="true" + ((++BUILD_WARNING_COUNT)) + echo ">> Lagoon detected services that have been removed from the docker-compose file" + if [ "$(featureFlag CLEANUP_REMOVED_LAGOON_SERVICES)" != enabled ]; then + echo "> You can remove these in the next build by setting the flag 'LAGOON_FEATURE_FLAG_CLEANUP_REMOVED_LAGOON_SERVICES=enabled' as a GLOBAL scoped variable to this environment or project" + fi + for DS in ${DELETE_SERVICE[@]} + do + if [ "$(featureFlag CLEANUP_REMOVED_LAGOON_SERVICES)" = enabled ]; then + echo ">> Removing deployment ${DS}" + if kubectl -n ${NAMESPACE} get deployments ${DS} &> /dev/null; then + kubectl -n ${NAMESPACE} delete deployment ${DS} + fi + if kubectl -n ${NAMESPACE} get service ${DS} &> /dev/null; then + echo ">>> Removing associated service ${DS}" + kubectl -n ${NAMESPACE} delete service ${DS} + fi + if kubectl -n ${NAMESPACE} get ingress ${DS} &> /dev/null; then + echo ">>> Removing associated ingress ${DS}" + kubectl -n ${NAMESPACE} delete ingress ${DS} + fi + if kubectl -n ${NAMESPACE} get pvc ${DS} &> /dev/null; then + echo ">>> Removing associated persistent volume ${DS}" + kubectl -n ${NAMESPACE} delete pvc ${DS} + fi + #delete anything else? + else + echo ">> The deployment '${DS}' would be removed" + if kubectl -n ${NAMESPACE} get service ${DS} &> /dev/null; then + echo ">>> The associated service '${DS}' would be removed" + fi + if kubectl -n ${NAMESPACE} get ingress ${DS} &> /dev/null; then + echo ">>> The associated ingress '${DS}' would be removed" + fi + if kubectl -n ${NAMESPACE} get pvc ${DS} &> /dev/null; then + echo ">>> The associated persistent volume '${DS}' would be removed" + fi + fi + done +else + echo "No service cleanup required" +fi + +currentStepEnd="$(date +"%Y-%m-%d %H:%M:%S")" +patchBuildStep "${buildStartTime}" "${previousStepEnd}" "${currentStepEnd}" "${NAMESPACE}" "serviceCleanupComplete" "Service/Deployment Cleanup" "${SERVICE_CLEANUP_WARNINGS}" +previousStepEnd=${currentStepEnd} beginBuildStep "Cronjob Cleanup" "cleaningUpCronjobs" set -x