diff --git a/cmd/template_autogen_ingress_test.go b/cmd/template_autogen_ingress_test.go index 9d9ffab9..6af67d52 100644 --- a/cmd/template_autogen_ingress_test.go +++ b/cmd/template_autogen_ingress_test.go @@ -3,7 +3,6 @@ package cmd import ( "fmt" "io/fs" - "io/ioutil" "os" "reflect" "testing" @@ -643,14 +642,14 @@ func TestAutogeneratedIngressGeneration(t *testing.T) { t.Errorf("AutogeneratedIngressGeneration() error = %v, wantErr %v", err, tt.wantErr) } - files, err := ioutil.ReadDir(savedTemplates) + files, err := os.ReadDir(savedTemplates) if err != nil { t.Errorf("couldn't read directory %v: %v", savedTemplates, err) } resultSize := 0 - results := []fs.FileInfo{} + results := []fs.DirEntry{} if !tt.emptyDir { - results, err = ioutil.ReadDir(tt.want) + results, err = os.ReadDir(tt.want) if err != nil { t.Errorf("couldn't read directory %v: %v", tt.want, err) } diff --git a/cmd/template_backups_test.go b/cmd/template_backups_test.go index 8a3be857..eaadfa21 100644 --- a/cmd/template_backups_test.go +++ b/cmd/template_backups_test.go @@ -2,7 +2,6 @@ package cmd import ( "fmt" - "io/ioutil" "os" "reflect" "testing" @@ -275,11 +274,11 @@ func TestBackupTemplateGeneration(t *testing.T) { if err := BackupTemplateGeneration(generator); (err != nil) != tt.wantErr { t.Errorf("BackupTemplateGeneration() error = %v, wantErr %v", err, tt.wantErr) } - files, err := ioutil.ReadDir(savedTemplates) + files, err := os.ReadDir(savedTemplates) if err != nil { t.Errorf("couldn't read directory %v: %v", savedTemplates, err) } - results, err := ioutil.ReadDir(tt.want) + results, err := os.ReadDir(tt.want) if err != nil { t.Errorf("couldn't read directory %v: %v", tt.want, err) } diff --git a/cmd/template_dbaas_test.go b/cmd/template_dbaas_test.go index bf205e0e..9338ba02 100644 --- a/cmd/template_dbaas_test.go +++ b/cmd/template_dbaas_test.go @@ -2,7 +2,6 @@ package cmd import ( "fmt" - "io/ioutil" "os" "reflect" "testing" @@ -172,11 +171,11 @@ func TestDBaaSTemplateGeneration(t *testing.T) { if err := DBaaSTemplateGeneration(generator); (err != nil) != tt.wantErr { t.Errorf("DBaaSTemplateGeneration() error = %v, wantErr %v", err, tt.wantErr) } - files, err := ioutil.ReadDir(savedTemplates) + files, err := os.ReadDir(savedTemplates) if err != nil { t.Errorf("couldn't read directory %v: %v", savedTemplates, err) } - results, err := ioutil.ReadDir(tt.want) + results, err := os.ReadDir(tt.want) if err != nil { t.Errorf("couldn't read directory %v: %v", tt.want, err) } diff --git a/cmd/template_ingress_test.go b/cmd/template_ingress_test.go index 20a44562..a17c4726 100644 --- a/cmd/template_ingress_test.go +++ b/cmd/template_ingress_test.go @@ -2,7 +2,6 @@ package cmd import ( "fmt" - "io/ioutil" "os" "reflect" "testing" @@ -568,11 +567,11 @@ func TestTemplateRoutes(t *testing.T) { t.Errorf("%v", err) } - files, err := ioutil.ReadDir(savedTemplates) + files, err := os.ReadDir(savedTemplates) if err != nil { t.Errorf("couldn't read directory %v: %v", savedTemplates, err) } - results, err := ioutil.ReadDir(tt.want) + results, err := os.ReadDir(tt.want) if err != nil { t.Errorf("couldn't read directory %v: %v", tt.want, err) } diff --git a/cmd/template_resourceworkloads.go b/cmd/template_resourceworkloads.go new file mode 100644 index 00000000..305a831c --- /dev/null +++ b/cmd/template_resourceworkloads.go @@ -0,0 +1,58 @@ +package cmd + +import ( + "fmt" + + "github.com/spf13/cobra" + generator "github.com/uselagoon/build-deploy-tool/internal/generator" + "github.com/uselagoon/build-deploy-tool/internal/helpers" + hpatemplate "github.com/uselagoon/build-deploy-tool/internal/templating/resources/hpa" + pdbtemplate "github.com/uselagoon/build-deploy-tool/internal/templating/resources/pdb" +) + +var resourceWorkloadGeneration = &cobra.Command{ + Use: "resource-workloads", + Aliases: []string{"rw"}, + Short: "Generate the resource workload templates for a Lagoon build", + RunE: func(cmd *cobra.Command, args []string) error { + generator, err := generatorInput(true) + if err != nil { + return err + } + return ResourceWorkloadTemplateGeneration(generator) + }, +} + +// IngressTemplateGeneration . +func ResourceWorkloadTemplateGeneration(g generator.GeneratorInput) error { + lagoonBuild, err := generator.NewGenerator( + g, + ) + if err != nil { + return err + } + savedTemplates := g.SavedTemplatesPath + + // generate the templates + if g.Debug { + fmt.Println(fmt.Sprintf("Templating HPA manifests to %s", fmt.Sprintf("%s/%s.yaml", savedTemplates, "hpas"))) + } + templateYAML, err := hpatemplate.GenerateHPATemplate(*lagoonBuild.BuildValues) + if err != nil { + return fmt.Errorf("couldn't generate template: %v", err) + } + helpers.WriteTemplateFile(fmt.Sprintf("%s/%s.yaml", savedTemplates, "hpas"), templateYAML) + if g.Debug { + fmt.Println(fmt.Sprintf("Templating HPA manifests to %s", fmt.Sprintf("%s/%s.yaml", savedTemplates, "pdbs"))) + } + templateYAML, err = pdbtemplate.GeneratePDBTemplate(*lagoonBuild.BuildValues) + if err != nil { + return fmt.Errorf("couldn't generate template: %v", err) + } + helpers.WriteTemplateFile(fmt.Sprintf("%s/%s.yaml", savedTemplates, "pdbs"), templateYAML) + return nil +} + +func init() { + templateCmd.AddCommand(resourceWorkloadGeneration) +} diff --git a/cmd/template_resourceworkloads_test.go b/cmd/template_resourceworkloads_test.go new file mode 100644 index 00000000..8099089d --- /dev/null +++ b/cmd/template_resourceworkloads_test.go @@ -0,0 +1,256 @@ +package cmd + +import ( + "fmt" + "os" + "reflect" + "testing" + "time" + + "github.com/uselagoon/build-deploy-tool/internal/dbaasclient" + "github.com/uselagoon/build-deploy-tool/internal/helpers" +) + +func TestResourceWorkloadTemplateGeneration(t *testing.T) { + type args struct { + alertContact string + statusPageID string + projectName string + environmentName string + branch string + prNumber string + prHeadBranch string + prBaseBranch string + environmentType string + buildType string + activeEnvironment string + standbyEnvironment string + cacheNoCache string + serviceID string + secretPrefix string + ingressClass string + projectVars string + envVars string + lagoonVersion string + lagoonYAML string + valuesFilePath string + templatePath string + workloadJSONfile string + } + tests := []struct { + name string + args args + want string + }{ + { + name: "test1 no resource workloads", + args: args{ + alertContact: "alertcontact", + statusPageID: "statuspageid", + projectName: "example-project", + environmentName: "main", + environmentType: "production", + buildType: "branch", + lagoonVersion: "v2.7.x", + branch: "main", + projectVars: `[{"name":"LAGOON_SYSTEM_ROUTER_PATTERN","value":"${service}-${project}-${environment}.example.com","scope":"internal_system"},{"name":"LAGOON_FASTLY_SERVICE_IDS","value":"example.com:service-id:true:annotationscom","scope":"build"}]`, + envVars: `[]`, + lagoonYAML: "../test-resources/template-resources/test1/lagoon.yml", + templatePath: "../test-resources/template-resources/output", + }, + want: "../test-resources/template-resources/test1-results", + }, + { + name: "test2 node hpa", + args: args{ + alertContact: "alertcontact", + statusPageID: "statuspageid", + projectName: "example-project", + environmentName: "main", + environmentType: "production", + buildType: "branch", + lagoonVersion: "v2.7.x", + branch: "main", + projectVars: `[{"name":"LAGOON_SYSTEM_ROUTER_PATTERN","value":"${service}-${project}-${environment}.example.com","scope":"internal_system"},{"name":"LAGOON_FASTLY_SERVICE_IDS","value":"example.com:service-id:true:annotationscom","scope":"build"}]`, + envVars: `[]`, + lagoonYAML: "../test-resources/template-resources/test2/lagoon.yml", + templatePath: "../test-resources/template-resources/output", + workloadJSONfile: "../test-resources/template-resources/test2/resources.json", + }, + want: "../test-resources/template-resources/test2-results", + }, + { + name: "test3 nginx hpa and pdb", + args: args{ + alertContact: "alertcontact", + statusPageID: "statuspageid", + projectName: "example-project", + environmentName: "main", + environmentType: "production", + buildType: "branch", + lagoonVersion: "v2.7.x", + branch: "main", + projectVars: `[{"name":"LAGOON_SYSTEM_ROUTER_PATTERN","value":"${service}-${project}-${environment}.example.com","scope":"internal_system"},{"name":"LAGOON_FASTLY_SERVICE_IDS","value":"example.com:service-id:true:annotationscom","scope":"build"}]`, + envVars: `[]`, + lagoonYAML: "../test-resources/template-resources/test3/lagoon.yml", + templatePath: "../test-resources/template-resources/output", + workloadJSONfile: "../test-resources/template-resources/test3/resources.json", + }, + want: "../test-resources/template-resources/test3-results", + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + // set the environment variables from args + err := os.Setenv("MONITORING_ALERTCONTACT", tt.args.alertContact) + if err != nil { + t.Errorf("%v", err) + } + err = os.Setenv("MONITORING_STATUSPAGEID", tt.args.statusPageID) + if err != nil { + t.Errorf("%v", err) + } + err = os.Setenv("PROJECT", tt.args.projectName) + if err != nil { + t.Errorf("%v", err) + } + err = os.Setenv("ENVIRONMENT", tt.args.environmentName) + if err != nil { + t.Errorf("%v", err) + } + err = os.Setenv("BRANCH", tt.args.branch) + if err != nil { + t.Errorf("%v", err) + } + err = os.Setenv("PR_NUMBER", tt.args.prNumber) + if err != nil { + t.Errorf("%v", err) + } + err = os.Setenv("PR_HEAD_BRANCH", tt.args.prHeadBranch) + if err != nil { + t.Errorf("%v", err) + } + err = os.Setenv("PR_BASE_BRANCH", tt.args.prBaseBranch) + if err != nil { + t.Errorf("%v", err) + } + err = os.Setenv("ENVIRONMENT_TYPE", tt.args.environmentType) + if err != nil { + t.Errorf("%v", err) + } + err = os.Setenv("BUILD_TYPE", tt.args.buildType) + if err != nil { + t.Errorf("%v", err) + } + err = os.Setenv("ACTIVE_ENVIRONMENT", tt.args.activeEnvironment) + if err != nil { + t.Errorf("%v", err) + } + err = os.Setenv("STANDBY_ENVIRONMENT", tt.args.standbyEnvironment) + if err != nil { + t.Errorf("%v", err) + } + err = os.Setenv("LAGOON_FASTLY_NOCACHE_SERVICE_ID", tt.args.cacheNoCache) + if err != nil { + t.Errorf("%v", err) + } + err = os.Setenv("LAGOON_PROJECT_VARIABLES", tt.args.projectVars) + if err != nil { + t.Errorf("%v", err) + } + err = os.Setenv("LAGOON_ENVIRONMENT_VARIABLES", tt.args.envVars) + if err != nil { + t.Errorf("%v", err) + } + err = os.Setenv("LAGOON_VERSION", tt.args.lagoonVersion) + if err != nil { + t.Errorf("%v", err) + } + err = os.Setenv("LAGOON_FEATURE_FLAG_DEFAULT_INGRESS_CLASS", tt.args.ingressClass) + if err != nil { + t.Errorf("%v", err) + } + err = os.Setenv("LAGOON_FEATURE_FLAG_DEFAULT_WORKLOAD_RESOURCES", helpers.ReadFileBase64Encode(tt.args.workloadJSONfile)) + if err != nil { + t.Errorf("%v", err) + } + generator, err := generatorInput(false) + if err != nil { + t.Errorf("%v", err) + } + generator.LagoonYAML = tt.args.lagoonYAML + generator.SavedTemplatesPath = tt.args.templatePath + // add dbaasclient overrides for tests + generator.DBaaSClient = dbaasclient.NewClient(dbaasclient.Client{ + RetryMax: 5, + RetryWaitMin: time.Duration(10) * time.Millisecond, + RetryWaitMax: time.Duration(50) * time.Millisecond, + }) + + savedTemplates := tt.args.templatePath + err = os.MkdirAll(tt.args.templatePath, 0755) + if err != nil { + t.Errorf("couldn't create directory %v: %v", savedTemplates, err) + } + + defer os.RemoveAll(savedTemplates) + + err = ResourceWorkloadTemplateGeneration(generator) + if err != nil { + t.Errorf("%v", err) + } + + files, err := os.ReadDir(savedTemplates) + if err != nil { + t.Errorf("couldn't read directory %v: %v", savedTemplates, err) + } + results, err := os.ReadDir(tt.want) + if err != nil { + t.Errorf("couldn't read directory %v: %v", tt.want, err) + } + if len(files) != len(results) { + for _, f := range files { + f1, err := os.ReadFile(fmt.Sprintf("%s/%s", savedTemplates, f.Name())) + if err != nil { + t.Errorf("couldn't read file %v: %v", savedTemplates, err) + } + fmt.Println(string(f1)) + } + t.Errorf("number of generated templates doesn't match results %v/%v: %v", len(files), len(results), err) + } + fCount := 0 + for _, f := range files { + for _, r := range results { + if f.Name() == r.Name() { + fCount++ + f1, err := os.ReadFile(fmt.Sprintf("%s/%s", savedTemplates, f.Name())) + if err != nil { + t.Errorf("couldn't read file %v: %v", savedTemplates, err) + } + r1, err := os.ReadFile(fmt.Sprintf("%s/%s", tt.want, f.Name())) + if err != nil { + t.Errorf("couldn't read file %v: %v", tt.want, err) + } + if !reflect.DeepEqual(f1, r1) { + fmt.Println(string(f1)) + t.Errorf("resulting templates do not match") + } + } + } + } + if fCount != len(files) { + for _, f := range files { + f1, err := os.ReadFile(fmt.Sprintf("%s/%s", savedTemplates, f.Name())) + if err != nil { + t.Errorf("couldn't read file %v: %v", savedTemplates, err) + } + fmt.Println(string(f1)) + } + t.Errorf("resulting templates do not match") + } + t.Cleanup(func() { + helpers.UnsetEnvVars([]helpers.EnvironmentVariable{{Name: "LAGOON_FEATURE_FLAG_DEFAULT_INGRESS_CLASS"}}) + }) + }) + } +} diff --git a/internal/generator/buildvalues.go b/internal/generator/buildvalues.go index 90050891..7acebb75 100644 --- a/internal/generator/buildvalues.go +++ b/internal/generator/buildvalues.go @@ -3,54 +3,79 @@ package generator import ( "github.com/uselagoon/build-deploy-tool/internal/dbaasclient" "github.com/uselagoon/build-deploy-tool/internal/lagoon" + autoscalingv2 "k8s.io/api/autoscaling/v2" corev1 "k8s.io/api/core/v1" + policyv1 "k8s.io/api/policy/v1" ) // BuildValues is the values file data generated by the lagoon build type BuildValues struct { - Project string `json:"project"` - Environment string `json:"environment"` - EnvironmentType string `json:"environmentType"` - Namespace string `json:"namespace"` - GitSha string `json:"gitSha"` - BuildType string `json:"buildType"` - Kubernetes string `json:"kubernetes"` - LagoonVersion string `json:"lagoonVersion"` - ActiveEnvironment string `json:"activeEnvironment"` - StandbyEnvironment string `json:"standbyEnvironment"` - IsActiveEnvironment bool `json:"isActiveEnvironment"` - IsStandbyEnvironment bool `json:"isStandbyEnvironment"` - PodSecurityContext *corev1.PodSecurityContext `json:"podSecurityContext"` - ImagePullSecrets []string `json:"imagePullSecrets"` - PrivateRegistryURLS []string `json:"privateRegistryURLS"` - Branch string `json:"branch"` - PRNumber string `json:"prNumber"` - PRTitle string `json:"prTitle"` - PRHeadBranch string `json:"prHeadBranch"` - PRBaseBranch string `json:"prBaseBranch"` - Fastly Fastly `json:"fastly"` - FastlyCacheNoCache string `json:"fastlyCacheNoCahce"` - FastlyAPISecretPrefix string `json:"fastlyAPISecretPrefix"` - ConfigMapSha string `json:"configMapSha"` - Route string `json:"route"` - Routes []string `json:"routes"` - AutogeneratedRoutes []string `json:"autogeneratedRoutes"` - RoutesAutogeneratePrefixes []string `json:"routesAutogeneratePrefixes"` - AutogeneratedRoutesFastly bool `json:"autogeneratedRoutesFastly"` - Services []ServiceValues `json:"services"` - Backup BackupConfiguration `json:"backup"` - Monitoring MonitoringConfig `json:"monitoring"` - DBaaSOperatorEndpoint string `json:"dbaasOperatorEndpoint"` - ServiceTypeOverrides *lagoon.EnvironmentVariable `json:"serviceTypeOverrides"` - DBaaSEnvironmentTypeOverrides *lagoon.EnvironmentVariable `json:"dbaasEnvironmentTypeOverrides"` - DBaaSFallbackSingle bool `json:"dbaasFallbackSingle"` - IngressClass string `json:"ingressClass"` - TaskScaleMaxIterations int `json:"taskScaleMaxIterations"` - TaskScaleWaitTime int `json:"taskScaleWaitTime"` - ImageCache string `json:"imageCache"` - DBaaSClient *dbaasclient.Client `json:"-"` + Project string `json:"project"` + Environment string `json:"environment"` + EnvironmentType string `json:"environmentType"` + Namespace string `json:"namespace"` + GitSha string `json:"gitSha"` + BuildType string `json:"buildType"` + Kubernetes string `json:"kubernetes"` + LagoonVersion string `json:"lagoonVersion"` + ActiveEnvironment string `json:"activeEnvironment"` + StandbyEnvironment string `json:"standbyEnvironment"` + IsActiveEnvironment bool `json:"isActiveEnvironment"` + IsStandbyEnvironment bool `json:"isStandbyEnvironment"` + PodSecurityContext *corev1.PodSecurityContext `json:"podSecurityContext"` + ImagePullSecrets []string `json:"imagePullSecrets"` + PrivateRegistryURLS []string `json:"privateRegistryURLS"` + Branch string `json:"branch"` + PRNumber string `json:"prNumber"` + PRTitle string `json:"prTitle"` + PRHeadBranch string `json:"prHeadBranch"` + PRBaseBranch string `json:"prBaseBranch"` + Fastly Fastly `json:"fastly"` + FastlyCacheNoCache string `json:"fastlyCacheNoCahce"` + FastlyAPISecretPrefix string `json:"fastlyAPISecretPrefix"` + ConfigMapSha string `json:"configMapSha"` + Route string `json:"route"` + Routes []string `json:"routes"` + AutogeneratedRoutes []string `json:"autogeneratedRoutes"` + RoutesAutogeneratePrefixes []string `json:"routesAutogeneratePrefixes"` + AutogeneratedRoutesFastly bool `json:"autogeneratedRoutesFastly"` + Services []ServiceValues `json:"services"` + Backup BackupConfiguration `json:"backup"` + Monitoring MonitoringConfig `json:"monitoring"` + DBaaSOperatorEndpoint string `json:"dbaasOperatorEndpoint"` + ServiceTypeOverrides *lagoon.EnvironmentVariable `json:"serviceTypeOverrides"` + DBaaSEnvironmentTypeOverrides *lagoon.EnvironmentVariable `json:"dbaasEnvironmentTypeOverrides"` + DBaaSFallbackSingle bool `json:"dbaasFallbackSingle"` + IngressClass string `json:"ingressClass"` + TaskScaleMaxIterations int `json:"taskScaleMaxIterations"` + TaskScaleWaitTime int `json:"taskScaleWaitTime"` + ImageCache string `json:"imageCache"` + DBaaSClient *dbaasclient.Client `json:"-"` + ResourceWorkloads map[string]ResourceWorkloads `json:"resourceWorkloads"` } +type ResourceWorkloads struct { + ServiceType string `json:"serviceType"` + HPA *HPASpec `json:"hpa,omitempty"` + PDB *PDBSpec `json:"pdb,omitempty"` + Resources []Resource `json:"resources"` +} + +type Resource struct { + Name string `json:"name"` + Resources corev1.ResourceRequirements `json:"resources"` +} + +type HPASpec struct { + Spec autoscalingv2.HorizontalPodAutoscalerSpec `json:"spec"` +} + +type PDBSpec struct { + Spec policyv1.PodDisruptionBudgetSpec `json:"spec"` +} + +type Resources map[string]corev1.ResourceRequirements + type Fastly struct { ServiceID string `json:"serviceId"` APISecretName string `json:"apiSecretName"` @@ -90,6 +115,7 @@ type ServiceValues struct { CronjobTolerations *[]corev1.Toleration `json:"cronjobTolerations"` CronjobAffinity *corev1.Affinity `json:"cronjobAffinity"` DBaasReadReplica bool `json:"dBaasReadReplica"` + ResourceWorkload string `json:"resourceWorkload,omitempty"` } // CronjobValues is the values for cronjobs diff --git a/internal/generator/generator.go b/internal/generator/generator.go index 2402f26b..87e722d7 100644 --- a/internal/generator/generator.go +++ b/internal/generator/generator.go @@ -233,6 +233,11 @@ func NewGenerator( } /* end backups configuration */ + /* calculate resource workloads */ + resourceWorkloads, err := getResourcesFromAPIEnvVar(lagoonEnvVars, generator.Debug) + buildValues.ResourceWorkloads = *resourceWorkloads + /* end resource workload calculation */ + /* start compose->service configuration */ err = generateServicesFromDockerCompose(&buildValues, lYAML, lagoonEnvVars, generator.IgnoreNonStringKeyErrors, generator.IgnoreMissingEnvFiles, generator.Debug) if err != nil { diff --git a/internal/generator/resources.go b/internal/generator/resources.go new file mode 100644 index 00000000..e9fbcc86 --- /dev/null +++ b/internal/generator/resources.go @@ -0,0 +1,41 @@ +package generator + +import ( + "encoding/base64" + "encoding/json" + "fmt" + + "github.com/uselagoon/build-deploy-tool/internal/lagoon" +) + +func getResourcesFromAPIEnvVar( + envVars []lagoon.EnvironmentVariable, + debug bool, +) (*map[string]ResourceWorkloads, error) { + resWorkloads := &map[string]ResourceWorkloads{} + // TODO: this is still to be determined how the data will be consumed from the API, it may eventually come from + // a configmap or some other means, or a combination of configmap and envvar merging + // for now, consume from featureflag var + resourceWorkloadsJSON := CheckFeatureFlag("WORKLOAD_RESOURCES", envVars, debug) + // only from envvar from api, not feature flagable + // resourceWorkloadsJSONvar, _ := lagoon.GetLagoonVariable("LAGOON_WORKLOAD_RESOURCES", []string{"build", "global"}, envVars) + // if resourceWorkloadsJSONvar != nil { + // resourceWorkloadsJSON = resourceWorkloadsJSONvar.Value + // } + if resourceWorkloadsJSON != "" { + if debug { + fmt.Println("Collecting resource workloads from WORKLOAD_RESOURCES variable") + } + // if the routesJSON is populated, then attempt to decode and unmarshal it + rawJSONStr, err := base64.StdEncoding.DecodeString(resourceWorkloadsJSON) + if err != nil { + return nil, fmt.Errorf("couldn't decode resource workloads from Lagoon API, is it actually base64 encoded?: %v", err) + } + rawJSON := []byte(rawJSONStr) + err = json.Unmarshal(rawJSON, resWorkloads) + if err != nil { + return nil, fmt.Errorf("couldn't unmarshal resource workloads from Lagoon API, is it actually JSON that has been base64 encoded?: %v", err) + } + } + return resWorkloads, nil +} diff --git a/internal/generator/resources_test.go b/internal/generator/resources_test.go new file mode 100644 index 00000000..014ff5f3 --- /dev/null +++ b/internal/generator/resources_test.go @@ -0,0 +1,163 @@ +package generator + +import ( + "encoding/json" + "reflect" + "testing" + + autoscalingv2 "k8s.io/api/autoscaling/v2" + corev1 "k8s.io/api/core/v1" + policyv1 "k8s.io/api/policy/v1" + "k8s.io/apimachinery/pkg/api/resource" + "k8s.io/apimachinery/pkg/util/intstr" + + "github.com/uselagoon/build-deploy-tool/internal/helpers" + "github.com/uselagoon/build-deploy-tool/internal/lagoon" +) + +func Test_getResourcesFromAPIEnvVar(t *testing.T) { + type args struct { + envVars []lagoon.EnvironmentVariable + debug bool + } + tests := []struct { + name string + args args + want *map[string]ResourceWorkloads + wantErr bool + }{ + { + name: "test1 - check that a scaling parameters are correctly defined", + args: args{ + envVars: []lagoon.EnvironmentVariable{ + { + Name: "LAGOON_FEATURE_FLAG_WORKLOAD_RESOURCES", + Value: helpers.ReadFileBase64Encode("test-resources/resources/test1-workload.json"), + Scope: "global", + }, + }, + }, + want: &map[string]ResourceWorkloads{ + "nginx": { + ServiceType: "nginx", + HPA: &HPASpec{ + Spec: autoscalingv2.HorizontalPodAutoscalerSpec{ + MinReplicas: helpers.Int32Ptr(8), + MaxReplicas: *helpers.Int32Ptr(16), + Metrics: []autoscalingv2.MetricSpec{ + { + Type: autoscalingv2.ResourceMetricSourceType, + Resource: &autoscalingv2.ResourceMetricSource{ + Name: corev1.ResourceCPU, + Target: autoscalingv2.MetricTarget{ + Type: autoscalingv2.UtilizationMetricType, + AverageUtilization: helpers.Int32Ptr(3000), + }, + }, + }, + }, + }, + }, + PDB: &PDBSpec{ + Spec: policyv1.PodDisruptionBudgetSpec{ + MinAvailable: &intstr.IntOrString{ + IntVal: 1, + Type: intstr.Int, + }, + }, + }, + Resources: []Resource{ + { + Name: "php", + Resources: corev1.ResourceRequirements{ + Requests: corev1.ResourceList{ + corev1.ResourceCPU: resource.MustParse("10m"), + corev1.ResourceMemory: resource.MustParse("10Mi"), + }, + }, + }, + { + Name: "nginx", + Resources: corev1.ResourceRequirements{ + Requests: corev1.ResourceList{ + corev1.ResourceCPU: resource.MustParse("10m"), + corev1.ResourceMemory: resource.MustParse("10Mi"), + }, + }, + }, + }, + }, + }, + }, + { + name: "test2 - check that a scaling parameters are correctly defined for multiple services", + args: args{ + envVars: []lagoon.EnvironmentVariable{ + { + Name: "LAGOON_FEATURE_FLAG_WORKLOAD_RESOURCES", + Value: helpers.ReadFileBase64Encode("test-resources/resources/test2-workload.json"), + Scope: "global", + }, + }, + }, + want: &map[string]ResourceWorkloads{ + "nginx": { + ServiceType: "nginx", + HPA: &HPASpec{ + Spec: autoscalingv2.HorizontalPodAutoscalerSpec{ + MinReplicas: helpers.Int32Ptr(8), + MaxReplicas: *helpers.Int32Ptr(16), + Metrics: []autoscalingv2.MetricSpec{ + { + Type: autoscalingv2.ResourceMetricSourceType, + Resource: &autoscalingv2.ResourceMetricSource{ + Name: corev1.ResourceCPU, + Target: autoscalingv2.MetricTarget{ + Type: autoscalingv2.UtilizationMetricType, + AverageUtilization: helpers.Int32Ptr(3000), + }, + }, + }, + }, + }, + }, + }, + "node": { + ServiceType: "node", + HPA: &HPASpec{ + Spec: autoscalingv2.HorizontalPodAutoscalerSpec{ + MinReplicas: helpers.Int32Ptr(8), + MaxReplicas: *helpers.Int32Ptr(16), + Metrics: []autoscalingv2.MetricSpec{ + { + Type: autoscalingv2.ResourceMetricSourceType, + Resource: &autoscalingv2.ResourceMetricSource{ + Name: corev1.ResourceCPU, + Target: autoscalingv2.MetricTarget{ + Type: autoscalingv2.UtilizationMetricType, + AverageUtilization: helpers.Int32Ptr(3000), + }, + }, + }, + }, + }, + }, + }, + }, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + got, err := getResourcesFromAPIEnvVar(tt.args.envVars, tt.args.debug) + if (err != nil) != tt.wantErr { + t.Errorf("getResourcesFromAPIEnvVar() error = %v, wantErr %v", err, tt.wantErr) + return + } + lValues, _ := json.Marshal(got) + wValues, _ := json.Marshal(tt.want) + if !reflect.DeepEqual(string(lValues), string(wValues)) { + t.Errorf("getResourcesFromAPIEnvVar() = %v, want %v", string(lValues), string(wValues)) + } + }) + } +} diff --git a/internal/generator/services.go b/internal/generator/services.go index 848a6575..9c1a76b7 100644 --- a/internal/generator/services.go +++ b/internal/generator/services.go @@ -37,35 +37,35 @@ var supportedAutogeneratedTypes = []string{ // just some default values for services var defaultServiceValues = map[string]map[string]string{ - "elasticsearch": map[string]string{ + "elasticsearch": { "persistentPath": "/usr/share/elasticsearch/data", "persistentSize": "5Gi", }, - "opensearch": map[string]string{ + "opensearch": { "persistentPath": "/usr/share/opensearch/data", "persistentSize": "5Gi", }, - "mariadb-single": map[string]string{ + "mariadb-single": { "persistentPath": "/var/lib/mysql", "persistentSize": "5Gi", }, - "postgres-single": map[string]string{ + "postgres-single": { "persistentPath": "/var/lib/postgresql/data", "persistentSize": "5Gi", }, - "mongodb-single": map[string]string{ + "mongodb-single": { "persistentPath": "/data/db", "persistentSize": "5Gi", }, - "varnish-persistent": map[string]string{ + "varnish-persistent": { "persistentPath": "/var/cache/varnish", "persistentSize": "5Gi", }, - "rabbitmq": map[string]string{ + "rabbitmq": { "persistentPath": "/var/lib/rabbitmq", "persistentSize": "5Gi", }, - "redis-persistent": map[string]string{ + "redis-persistent": { "persistentPath": "/data", "persistentSize": "5Gi", }, @@ -305,6 +305,25 @@ func composeToServiceValues( } } + // check if the service has a scalingparameter request + serviceResourceWorkload := lagoon.CheckServiceLagoonLabel(composeServiceValues.Labels, "lagoon.scalingparameter") + if serviceResourceWorkload != "" { + // check if the requested resource workload actually exists + resWork, ok := buildValues.ResourceWorkloads[serviceResourceWorkload] + if !ok { + return ServiceValues{}, fmt.Errorf( + "The requested scaling parameter group %s for service %s is not valid", + serviceResourceWorkload, composeService, + ) + } + if resWork.ServiceType != lagoonType { + return ServiceValues{}, fmt.Errorf( + "The requested scaling parameter group %s for service %s is not valid for this service type %s", + serviceResourceWorkload, composeService, lagoonType, + ) + } + } + // check if this service is one that supports autogenerated routes if !helpers.Contains(supportedAutogeneratedTypes, lagoonType) { autogenEnabled = false @@ -322,6 +341,7 @@ func composeToServiceValues( PersistentVolumePath: servicePersistentPath, PersistentVolumeName: servicePersistentName, PersistentVolumeSize: servicePersistentSize, + ResourceWorkload: serviceResourceWorkload, } // check if the service has a service port override (this only applies to basic(-persistent)) servicePortOverride := lagoon.CheckServiceLagoonLabel(composeServiceValues.Labels, "lagoon.service.port") diff --git a/internal/generator/services_test.go b/internal/generator/services_test.go index 03fb9b59..1b0de806 100644 --- a/internal/generator/services_test.go +++ b/internal/generator/services_test.go @@ -10,6 +10,8 @@ import ( "github.com/uselagoon/build-deploy-tool/internal/dbaasclient" "github.com/uselagoon/build-deploy-tool/internal/helpers" "github.com/uselagoon/build-deploy-tool/internal/lagoon" + autoscalingv2 "k8s.io/api/autoscaling/v2" + corev1 "k8s.io/api/core/v1" ) func Test_composeToServiceValues(t *testing.T) { @@ -485,6 +487,86 @@ func Test_composeToServiceValues(t *testing.T) { want: ServiceValues{}, wantErr: true, }, + { + name: "test15 - scaling parameter exists", + args: args{ + lYAML: &lagoon.YAML{ + Environments: lagoon.Environments{ + "main": lagoon.Environment{}, + }, + }, + buildValues: &BuildValues{ + Environment: "main", + Branch: "main", + BuildType: "branch", + ServiceTypeOverrides: &lagoon.EnvironmentVariable{}, + ResourceWorkloads: map[string]ResourceWorkloads{ + "nginx": { + ServiceType: "nginx", + HPA: &HPASpec{ + Spec: autoscalingv2.HorizontalPodAutoscalerSpec{ + MinReplicas: helpers.Int32Ptr(1), + MaxReplicas: 2, + Metrics: []autoscalingv2.MetricSpec{ + { + Type: autoscalingv2.ResourceMetricSourceType, + Resource: &autoscalingv2.ResourceMetricSource{ + Name: corev1.ResourceCPU, + Target: autoscalingv2.MetricTarget{ + Type: autoscalingv2.UtilizationMetricType, + AverageUtilization: helpers.Int32Ptr(3000), + }, + }, + }, + }, + }, + }, + }, + }, + }, + composeService: "nginx", + composeServiceValues: composetypes.ServiceConfig{ + Labels: composetypes.Labels{ + "lagoon.type": "nginx", + "lagoon.scalingparameter": "nginx", + }, + }, + }, + want: ServiceValues{ + Name: "nginx", + OverrideName: "nginx", + Type: "nginx", + AutogeneratedRoutesEnabled: true, + AutogeneratedRoutesTLSAcme: true, + ResourceWorkload: "nginx", + }, + }, + { + name: "test15 - scaling parameter doesn't exist (error)", + args: args{ + lYAML: &lagoon.YAML{ + Environments: lagoon.Environments{ + "main": lagoon.Environment{}, + }, + }, + buildValues: &BuildValues{ + Environment: "main", + Branch: "main", + BuildType: "branch", + ServiceTypeOverrides: &lagoon.EnvironmentVariable{}, + ResourceWorkloads: map[string]ResourceWorkloads{}, + }, + composeService: "nginx", + composeServiceValues: composetypes.ServiceConfig{ + Labels: composetypes.Labels{ + "lagoon.type": "nginx", + "lagoon.scalingparameter": "nginx", + }, + }, + }, + want: ServiceValues{}, + wantErr: true, + }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { diff --git a/internal/generator/test-resources/resources/test1-workload.json b/internal/generator/test-resources/resources/test1-workload.json new file mode 100644 index 00000000..8d5a2c3f --- /dev/null +++ b/internal/generator/test-resources/resources/test1-workload.json @@ -0,0 +1,48 @@ +{ + "nginx": { + "serviceType": "nginx", + "hpa": { + "spec": { + "minReplicas": 8, + "maxReplicas": 16, + "metrics": [ + { + "type": "Resource", + "resource": { + "name": "cpu", + "target": { + "type": "Utilization", + "averageUtilization": 3000 + } + } + } + ] + } + }, + "pdb": { + "spec": { + "minAvailable": 1 + } + }, + "resources": [ + { + "name": "php", + "resources": { + "requests": { + "cpu": "10m", + "memory": "10Mi" + } + } + }, + { + "name": "nginx", + "resources": { + "requests": { + "cpu": "10m", + "memory": "10Mi" + } + } + } + ] + } +} \ No newline at end of file diff --git a/internal/generator/test-resources/resources/test2-workload.json b/internal/generator/test-resources/resources/test2-workload.json new file mode 100644 index 00000000..517aaf71 --- /dev/null +++ b/internal/generator/test-resources/resources/test2-workload.json @@ -0,0 +1,44 @@ +{ + "nginx": { + "serviceType": "nginx", + "hpa": { + "spec": { + "minReplicas": 8, + "maxReplicas": 16, + "metrics": [ + { + "type": "Resource", + "resource": { + "name": "cpu", + "target": { + "type": "Utilization", + "averageUtilization": 3000 + } + } + } + ] + } + } + }, + "node": { + "serviceType": "node", + "hpa": { + "spec": { + "minReplicas": 8, + "maxReplicas": 16, + "metrics": [ + { + "type": "Resource", + "resource": { + "name": "cpu", + "target": { + "type": "Utilization", + "averageUtilization": 3000 + } + } + } + ] + } + } + } +} \ No newline at end of file diff --git a/internal/helpers/helpers.go b/internal/helpers/helpers.go index cd3b031a..3e6ddfae 100644 --- a/internal/helpers/helpers.go +++ b/internal/helpers/helpers.go @@ -5,6 +5,7 @@ import ( "crypto/md5" "crypto/sha256" "encoding/base32" + "encoding/base64" "encoding/gob" "encoding/hex" "fmt" @@ -219,3 +220,14 @@ func DeepCopy(src, dist interface{}) (err error) { } return gob.NewDecoder(&buf).Decode(dist) } + +// helper function to read a file and base64 encode the result +// returns an empty string on any error +// mainly used in tests to consume JSON or YAML test resources +func ReadFileBase64Encode(file string) string { + raw, err := os.ReadFile(file) + if err != nil { + return "" + } + return base64.StdEncoding.EncodeToString(raw) +} diff --git a/internal/templating/backups/template_prebackuppod.go b/internal/templating/backups/template_prebackuppod.go index b558c248..677053f3 100644 --- a/internal/templating/backups/template_prebackuppod.go +++ b/internal/templating/backups/template_prebackuppod.go @@ -54,8 +54,8 @@ func GeneratePreBackupPod( additionalAnnotations["lagoon.sh/prBaseBranch"] = lValues.PRBaseBranch } additionalLabels["app.kubernetes.io/name"] = serviceValues.Type - additionalLabels["app.kubernetes.io/instance"] = serviceValues.Name - additionalLabels["lagoon.sh/service"] = serviceValues.Name + additionalLabels["app.kubernetes.io/instance"] = serviceValues.OverrideName + additionalLabels["lagoon.sh/service"] = serviceValues.OverrideName additionalLabels["lagoon.sh/service-type"] = serviceValues.Type if _, ok := preBackupPodSpecs[serviceValues.Type]; ok { switch lValues.Backup.K8upVersion { @@ -66,14 +66,14 @@ func GeneratePreBackupPod( APIVersion: k8upv1alpha1.GroupVersion.String(), }, ObjectMeta: metav1.ObjectMeta{ - Name: fmt.Sprintf("%s-prebackuppod", serviceValues.Name), + Name: fmt.Sprintf("%s-prebackuppod", serviceValues.OverrideName), }, Spec: k8upv1alpha1.PreBackupPodSpec{}, } prebackuppod.ObjectMeta.Labels = labels prebackuppod.ObjectMeta.Annotations = annotations - prebackuppod.ObjectMeta.Labels["prebackuppod"] = serviceValues.Name + prebackuppod.ObjectMeta.Labels["prebackuppod"] = serviceValues.OverrideName var pbp bytes.Buffer tmpl, _ := template.New("").Funcs(funcMap).Parse(preBackupPodSpecs[serviceValues.Type]) diff --git a/internal/templating/dbaas/template_dbaas.go b/internal/templating/dbaas/template_dbaas.go index 74325022..1a979c01 100644 --- a/internal/templating/dbaas/template_dbaas.go +++ b/internal/templating/dbaas/template_dbaas.go @@ -57,9 +57,9 @@ func GenerateDBaaSTemplate( if helpers.Contains(dbaasTypes, serviceValues.Type) { var consumerBytes []byte additionalLabels["app.kubernetes.io/name"] = serviceValues.Type - additionalLabels["app.kubernetes.io/instance"] = serviceValues.Name + additionalLabels["app.kubernetes.io/instance"] = serviceValues.OverrideName additionalLabels["lagoon.sh/template"] = fmt.Sprintf("%s-%s", serviceValues.Type, "0.1.0") - additionalLabels["lagoon.sh/service"] = serviceValues.Name + additionalLabels["lagoon.sh/service"] = serviceValues.OverrideName additionalLabels["lagoon.sh/service-type"] = serviceValues.Type switch serviceValues.Type { case "mariadb-dbaas": @@ -70,7 +70,7 @@ func GenerateDBaaSTemplate( APIVersion: mariadbv1.GroupVersion.String(), }, ObjectMeta: metav1.ObjectMeta{ - Name: serviceValues.Name, + Name: serviceValues.OverrideName, }, Spec: mariadbv1.MariaDBConsumerSpec{ Environment: serviceValues.DBaaSEnvironment, @@ -88,13 +88,13 @@ func GenerateDBaaSTemplate( // validate any annotations if err := apivalidation.ValidateAnnotations(mariaDBConsumer.ObjectMeta.Annotations, nil); err != nil { if len(err) != 0 { - return nil, fmt.Errorf("the annotations for %s are not valid: %v", serviceValues.Name, err) + return nil, fmt.Errorf("the annotations for %s are not valid: %v", serviceValues.OverrideName, err) } } // validate any labels if err := metavalidation.ValidateLabels(mariaDBConsumer.ObjectMeta.Labels, nil); err != nil { if len(err) != 0 { - return nil, fmt.Errorf("the labels for %s are not valid: %v", serviceValues.Name, err) + return nil, fmt.Errorf("the labels for %s are not valid: %v", serviceValues.OverrideName, err) } } @@ -116,7 +116,7 @@ func GenerateDBaaSTemplate( APIVersion: mongodbv1.GroupVersion.String(), }, ObjectMeta: metav1.ObjectMeta{ - Name: serviceValues.Name, + Name: serviceValues.OverrideName, }, Spec: mongodbv1.MongoDBConsumerSpec{ Environment: serviceValues.DBaaSEnvironment, @@ -134,13 +134,13 @@ func GenerateDBaaSTemplate( // validate any annotations if err := apivalidation.ValidateAnnotations(mongodbConsumer.ObjectMeta.Annotations, nil); err != nil { if len(err) != 0 { - return nil, fmt.Errorf("the annotations for %s are not valid: %v", serviceValues.Name, err) + return nil, fmt.Errorf("the annotations for %s are not valid: %v", serviceValues.OverrideName, err) } } // validate any labels if err := metavalidation.ValidateLabels(mongodbConsumer.ObjectMeta.Labels, nil); err != nil { if len(err) != 0 { - return nil, fmt.Errorf("the labels for %s are not valid: %v", serviceValues.Name, err) + return nil, fmt.Errorf("the labels for %s are not valid: %v", serviceValues.OverrideName, err) } } // check length of labels @@ -161,7 +161,7 @@ func GenerateDBaaSTemplate( APIVersion: postgresv1.GroupVersion.String(), }, ObjectMeta: metav1.ObjectMeta{ - Name: serviceValues.Name, + Name: serviceValues.OverrideName, }, Spec: postgresv1.PostgreSQLConsumerSpec{ Environment: serviceValues.DBaaSEnvironment, @@ -179,13 +179,13 @@ func GenerateDBaaSTemplate( // validate any annotations if err := apivalidation.ValidateAnnotations(postgresqlConsumer.ObjectMeta.Annotations, nil); err != nil { if len(err) != 0 { - return nil, fmt.Errorf("the annotations for %s are not valid: %v", serviceValues.Name, err) + return nil, fmt.Errorf("the annotations for %s are not valid: %v", serviceValues.OverrideName, err) } } // validate any labels if err := metavalidation.ValidateLabels(postgresqlConsumer.ObjectMeta.Labels, nil); err != nil { if len(err) != 0 { - return nil, fmt.Errorf("the labels for %s are not valid: %v", serviceValues.Name, err) + return nil, fmt.Errorf("the labels for %s are not valid: %v", serviceValues.OverrideName, err) } } diff --git a/internal/templating/resources/hpa/template_hpa.go b/internal/templating/resources/hpa/template_hpa.go new file mode 100644 index 00000000..f1335ac5 --- /dev/null +++ b/internal/templating/resources/hpa/template_hpa.go @@ -0,0 +1,119 @@ +package hpa + +import ( + "fmt" + + "github.com/uselagoon/build-deploy-tool/internal/generator" + "github.com/uselagoon/build-deploy-tool/internal/helpers" + + autoscalingv2 "k8s.io/api/autoscaling/v2" + apivalidation "k8s.io/apimachinery/pkg/api/validation" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + metavalidation "k8s.io/apimachinery/pkg/apis/meta/v1/validation" + + "sigs.k8s.io/yaml" +) + +func GenerateHPATemplate( + lValues generator.BuildValues, +) ([]byte, error) { + // generate the template spec + + var result []byte + separator := []byte("---\n") + + // add the default labels + labels := map[string]string{ + "app.kubernetes.io/managed-by": "build-deploy-tool", + "lagoon.sh/project": lValues.Project, + "lagoon.sh/environment": lValues.Environment, + "lagoon.sh/environmentType": lValues.EnvironmentType, + "lagoon.sh/buildType": lValues.BuildType, + } + + // add the default annotations + annotations := map[string]string{ + "lagoon.sh/version": lValues.LagoonVersion, + } + + // create the hpas + for _, serviceValues := range lValues.Services { + if serviceValues.ResourceWorkload != "" && (lValues.ResourceWorkloads[serviceValues.ResourceWorkload].HPA != nil) { + // add any additional labels + additionalLabels := map[string]string{} + additionalAnnotations := map[string]string{} + if lValues.BuildType == "branch" { + additionalAnnotations["lagoon.sh/branch"] = lValues.Branch + } else if lValues.BuildType == "pullrequest" { + additionalAnnotations["lagoon.sh/prNumber"] = lValues.PRNumber + additionalAnnotations["lagoon.sh/prHeadBranch"] = lValues.PRHeadBranch + additionalAnnotations["lagoon.sh/prBaseBranch"] = lValues.PRBaseBranch + } + additionalLabels["app.kubernetes.io/name"] = serviceValues.Type + additionalLabels["app.kubernetes.io/instance"] = serviceValues.OverrideName + additionalLabels["lagoon.sh/service"] = serviceValues.OverrideName + additionalLabels["lagoon.sh/service-type"] = serviceValues.Type + hpa := &autoscalingv2.HorizontalPodAutoscaler{ + TypeMeta: metav1.TypeMeta{ + Kind: "HorizontalPodAutoscaler", + APIVersion: autoscalingv2.SchemeGroupVersion.String(), + }, + ObjectMeta: metav1.ObjectMeta{ + Name: fmt.Sprintf("%s-hpa", serviceValues.OverrideName), + }, + Spec: lValues.ResourceWorkloads[serviceValues.ResourceWorkload].HPA.Spec, + } + + // set the scale target to the service that requested it + // since all lagoon deployed services are deployments at the moment + // default this set to the deployment kind, refactor in the future if lagoon supports + // additional types (statefulsets/daemonsets?) + hpa.Spec.ScaleTargetRef = autoscalingv2.CrossVersionObjectReference{ + Kind: "Deployment", + Name: serviceValues.OverrideName, + APIVersion: "apps/v1", + } + + hpa.ObjectMeta.Labels = labels + hpa.ObjectMeta.Annotations = annotations + + for key, value := range additionalLabels { + hpa.ObjectMeta.Labels[key] = value + } + // add any additional annotations + for key, value := range additionalAnnotations { + hpa.ObjectMeta.Annotations[key] = value + } + // validate any annotations + if err := apivalidation.ValidateAnnotations(hpa.ObjectMeta.Annotations, nil); err != nil { + if len(err) != 0 { + return nil, fmt.Errorf("the annotations for %s/%s are not valid: %v", "hpa", serviceValues.Name, err) + } + } + // validate any labels + if err := metavalidation.ValidateLabels(hpa.ObjectMeta.Labels, nil); err != nil { + if len(err) != 0 { + return nil, fmt.Errorf("the labels for %s/%s are not valid: %v", "hpa", serviceValues.Name, err) + } + } + + // check length of labels + err := helpers.CheckLabelLength(hpa.ObjectMeta.Labels) + if err != nil { + return nil, err + } + // @TODO: we should review this in the future when we stop doing `kubectl apply` in the builds :) + // marshal the resulting ingress + hpaBytes, err := yaml.Marshal(hpa) + if err != nil { + return nil, err + } + // add the seperator to the template so that it can be `kubectl apply` in bulk as part + // of the current build process + restoreResult := append(separator[:], hpaBytes[:]...) + result = append(result, restoreResult[:]...) + } + } + + return result, nil +} diff --git a/internal/templating/resources/hpa/template_hpa_test.go b/internal/templating/resources/hpa/template_hpa_test.go new file mode 100644 index 00000000..d543e5fd --- /dev/null +++ b/internal/templating/resources/hpa/template_hpa_test.go @@ -0,0 +1,139 @@ +package hpa + +import ( + "os" + "reflect" + "testing" + "time" + + "github.com/uselagoon/build-deploy-tool/internal/dbaasclient" + "github.com/uselagoon/build-deploy-tool/internal/generator" + "github.com/uselagoon/build-deploy-tool/internal/helpers" + autoscalingv2 "k8s.io/api/autoscaling/v2" + corev1 "k8s.io/api/core/v1" +) + +func TestGenerateHPATemplate(t *testing.T) { + type args struct { + lValues generator.BuildValues + } + tests := []struct { + name string + args args + want string + wantErr bool + }{ + { + name: "test1 - nginx hpa", + args: args{ + lValues: generator.BuildValues{ + Project: "example-project", + Environment: "brancha", + EnvironmentType: "production", + Namespace: "myexample-project-brancha", + BuildType: "branch", + LagoonVersion: "v2.x.x", + Kubernetes: "generator.local", + Branch: "brancha", + Services: []generator.ServiceValues{ + { + Name: "nginx", + OverrideName: "nginx", + Type: "nginx-php-persistent", + ResourceWorkload: "nginx-php-performance", + }, + }, + ResourceWorkloads: map[string]generator.ResourceWorkloads{ + "nginx": { + ServiceType: "nginx", + HPA: &generator.HPASpec{ + Spec: autoscalingv2.HorizontalPodAutoscalerSpec{ + MinReplicas: helpers.Int32Ptr(2), + MaxReplicas: *helpers.Int32Ptr(5), + Metrics: []autoscalingv2.MetricSpec{ + { + Type: autoscalingv2.ResourceMetricSourceType, + Resource: &autoscalingv2.ResourceMetricSource{ + Name: corev1.ResourceCPU, + Target: autoscalingv2.MetricTarget{ + Type: autoscalingv2.UtilizationMetricType, + AverageUtilization: helpers.Int32Ptr(3000), + }, + }, + }, + }}, + }, + }, + "nginx-php-performance": { + ServiceType: "nginx-php-persistent", + HPA: &generator.HPASpec{ + Spec: autoscalingv2.HorizontalPodAutoscalerSpec{ + MinReplicas: helpers.Int32Ptr(8), + MaxReplicas: *helpers.Int32Ptr(16), + Metrics: []autoscalingv2.MetricSpec{ + { + Type: autoscalingv2.ResourceMetricSourceType, + Resource: &autoscalingv2.ResourceMetricSource{ + Name: corev1.ResourceCPU, + Target: autoscalingv2.MetricTarget{ + Type: autoscalingv2.UtilizationMetricType, + AverageUtilization: helpers.Int32Ptr(1500), + }, + }, + }, + }}, + }, + }, + }, + }, + }, + want: "test-resources/result-nginx.yaml", + }, + { + name: "test2 - no resources", + args: args{ + lValues: generator.BuildValues{ + Project: "example-project", + Environment: "brancha", + EnvironmentType: "production", + Namespace: "myexample-project-brancha", + BuildType: "branch", + LagoonVersion: "v2.x.x", + Kubernetes: "generator.local", + Branch: "brancha", + Services: []generator.ServiceValues{ + { + Name: "nginx", + OverrideName: "nginx", + Type: "nginx-php-persistent", + }, + }, + ResourceWorkloads: map[string]generator.ResourceWorkloads{}, + }, + }, + want: "test-resources/result-no-resources.yaml", + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + // add dbaasclient overrides for tests + tt.args.lValues.DBaaSClient = dbaasclient.NewClient(dbaasclient.Client{ + RetryMax: 5, + RetryWaitMin: time.Duration(10) * time.Millisecond, + RetryWaitMax: time.Duration(50) * time.Millisecond, + }) + got, err := GenerateHPATemplate(tt.args.lValues) + if (err != nil) != tt.wantErr { + t.Errorf("GenerateHPATemplate() error = %v, wantErr %v", err, tt.wantErr) + return + } + r1, err := os.ReadFile(tt.want) + if err != nil { + t.Errorf("couldn't read file %v: %v", tt.want, err) + } + if !reflect.DeepEqual(string(got), string(r1)) { + t.Errorf("GenerateHPATemplate() = %v, want %v", string(got), string(r1)) + } + }) + } +} diff --git a/internal/templating/resources/hpa/test-resources/result-nginx.yaml b/internal/templating/resources/hpa/test-resources/result-nginx.yaml new file mode 100644 index 00000000..e0d3f3c4 --- /dev/null +++ b/internal/templating/resources/hpa/test-resources/result-nginx.yaml @@ -0,0 +1,36 @@ +--- +apiVersion: autoscaling/v2 +kind: HorizontalPodAutoscaler +metadata: + annotations: + lagoon.sh/branch: brancha + lagoon.sh/version: v2.x.x + creationTimestamp: null + labels: + app.kubernetes.io/instance: nginx + app.kubernetes.io/managed-by: build-deploy-tool + app.kubernetes.io/name: nginx-php-persistent + lagoon.sh/buildType: branch + lagoon.sh/environment: brancha + lagoon.sh/environmentType: production + lagoon.sh/project: example-project + lagoon.sh/service: nginx + lagoon.sh/service-type: nginx-php-persistent + name: nginx-hpa +spec: + maxReplicas: 16 + metrics: + - resource: + name: cpu + target: + averageUtilization: 1500 + type: Utilization + type: Resource + minReplicas: 8 + scaleTargetRef: + apiVersion: apps/v1 + kind: Deployment + name: nginx +status: + currentMetrics: null + desiredReplicas: 0 diff --git a/internal/templating/resources/hpa/test-resources/result-no-resources.yaml b/internal/templating/resources/hpa/test-resources/result-no-resources.yaml new file mode 100644 index 00000000..e69de29b diff --git a/internal/templating/resources/pdb/template_pdb.go b/internal/templating/resources/pdb/template_pdb.go new file mode 100644 index 00000000..74670136 --- /dev/null +++ b/internal/templating/resources/pdb/template_pdb.go @@ -0,0 +1,115 @@ +package pdb + +import ( + "fmt" + + "github.com/uselagoon/build-deploy-tool/internal/generator" + "github.com/uselagoon/build-deploy-tool/internal/helpers" + + policyv1 "k8s.io/api/policy/v1" + apivalidation "k8s.io/apimachinery/pkg/api/validation" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + metavalidation "k8s.io/apimachinery/pkg/apis/meta/v1/validation" + + "sigs.k8s.io/yaml" +) + +func GeneratePDBTemplate( + lValues generator.BuildValues, +) ([]byte, error) { + // generate the template spec + + var result []byte + separator := []byte("---\n") + + // add the default labels + labels := map[string]string{ + "app.kubernetes.io/managed-by": "build-deploy-tool", + "lagoon.sh/project": lValues.Project, + "lagoon.sh/environment": lValues.Environment, + "lagoon.sh/environmentType": lValues.EnvironmentType, + "lagoon.sh/buildType": lValues.BuildType, + } + + // add the default annotations + annotations := map[string]string{ + "lagoon.sh/version": lValues.LagoonVersion, + } + + // create the pdbs + for _, serviceValues := range lValues.Services { + if serviceValues.ResourceWorkload != "" && (lValues.ResourceWorkloads[serviceValues.ResourceWorkload].PDB != nil) { + // add any additional labels + additionalLabels := map[string]string{} + additionalAnnotations := map[string]string{} + if lValues.BuildType == "branch" { + additionalAnnotations["lagoon.sh/branch"] = lValues.Branch + } else if lValues.BuildType == "pullrequest" { + additionalAnnotations["lagoon.sh/prNumber"] = lValues.PRNumber + additionalAnnotations["lagoon.sh/prHeadBranch"] = lValues.PRHeadBranch + additionalAnnotations["lagoon.sh/prBaseBranch"] = lValues.PRBaseBranch + } + additionalLabels["app.kubernetes.io/name"] = serviceValues.Type + additionalLabels["app.kubernetes.io/instance"] = serviceValues.OverrideName + additionalLabels["lagoon.sh/service"] = serviceValues.OverrideName + additionalLabels["lagoon.sh/service-type"] = serviceValues.Type + pdb := &policyv1.PodDisruptionBudget{ + TypeMeta: metav1.TypeMeta{ + Kind: "PodDisruptionBudget", + APIVersion: policyv1.SchemeGroupVersion.String(), + }, + ObjectMeta: metav1.ObjectMeta{ + Name: fmt.Sprintf("%s-pdb", serviceValues.OverrideName), + }, + Spec: lValues.ResourceWorkloads[serviceValues.ResourceWorkload].PDB.Spec, + } + + // set the selector target to the service that requested it) + pdb.Spec.Selector = &metav1.LabelSelector{ + MatchLabels: map[string]string{ + "lagoon.sh/service": serviceValues.OverrideName, + }, + } + + pdb.ObjectMeta.Labels = labels + pdb.ObjectMeta.Annotations = annotations + + for key, value := range additionalLabels { + pdb.ObjectMeta.Labels[key] = value + } + // add any additional annotations + for key, value := range additionalAnnotations { + pdb.ObjectMeta.Annotations[key] = value + } + // validate any annotations + if err := apivalidation.ValidateAnnotations(pdb.ObjectMeta.Annotations, nil); err != nil { + if len(err) != 0 { + return nil, fmt.Errorf("the annotations for %s/%s are not valid: %v", "pdb", serviceValues.Name, err) + } + } + // validate any labels + if err := metavalidation.ValidateLabels(pdb.ObjectMeta.Labels, nil); err != nil { + if len(err) != 0 { + return nil, fmt.Errorf("the labels for %s/%s are not valid: %v", "pdb", serviceValues.Name, err) + } + } + + // check length of labels + err := helpers.CheckLabelLength(pdb.ObjectMeta.Labels) + if err != nil { + return nil, err + } + // @TODO: we should review this in the future when we stop doing `kubectl apply` in the builds :) + // marshal the resulting ingress + pdbBytes, err := yaml.Marshal(pdb) + if err != nil { + return nil, err + } + // add the seperator to the template so that it can be `kubectl apply` in bulk as part + // of the current build process + restoreResult := append(separator[:], pdbBytes[:]...) + result = append(result, restoreResult[:]...) + } + } + return result, nil +} diff --git a/internal/templating/resources/pdb/template_pdb_test.go b/internal/templating/resources/pdb/template_pdb_test.go new file mode 100644 index 00000000..10ed8ec5 --- /dev/null +++ b/internal/templating/resources/pdb/template_pdb_test.go @@ -0,0 +1,97 @@ +package pdb + +import ( + "os" + "reflect" + "testing" + "time" + + "github.com/uselagoon/build-deploy-tool/internal/dbaasclient" + "github.com/uselagoon/build-deploy-tool/internal/generator" + policyv1 "k8s.io/api/policy/v1" + "k8s.io/apimachinery/pkg/util/intstr" +) + +func TestGeneratePDBTemplate(t *testing.T) { + type args struct { + lValues generator.BuildValues + } + tests := []struct { + name string + args args + want string + wantErr bool + }{ + { + name: "test1 - nginx pdb", + args: args{ + lValues: generator.BuildValues{ + Project: "example-project", + Environment: "brancha", + EnvironmentType: "production", + Namespace: "myexample-project-brancha", + BuildType: "branch", + LagoonVersion: "v2.x.x", + Kubernetes: "generator.local", + Branch: "brancha", + Services: []generator.ServiceValues{ + { + Name: "nginx", + OverrideName: "nginx", + Type: "nginx-php-persistent", + ResourceWorkload: "nginx-php-performance", + }, + }, + ResourceWorkloads: map[string]generator.ResourceWorkloads{ + "nginx": { + ServiceType: "nginx", + PDB: &generator.PDBSpec{ + Spec: policyv1.PodDisruptionBudgetSpec{ + MinAvailable: &intstr.IntOrString{ + IntVal: 1, + Type: intstr.Int, + }, + }, + }, + }, + "nginx-php-performance": { + ServiceType: "nginx-php-persistent", + + PDB: &generator.PDBSpec{ + Spec: policyv1.PodDisruptionBudgetSpec{ + MinAvailable: &intstr.IntOrString{ + IntVal: 3, + Type: intstr.Int, + }, + }, + }, + }, + }, + }, + }, + want: "test-resources/result-nginx.yaml", + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + // add dbaasclient overrides for tests + tt.args.lValues.DBaaSClient = dbaasclient.NewClient(dbaasclient.Client{ + RetryMax: 5, + RetryWaitMin: time.Duration(10) * time.Millisecond, + RetryWaitMax: time.Duration(50) * time.Millisecond, + }) + got, err := GeneratePDBTemplate(tt.args.lValues) + if (err != nil) != tt.wantErr { + t.Errorf("GeneratePDBTemplate() error = %v, wantErr %v", err, tt.wantErr) + return + } + r1, err := os.ReadFile(tt.want) + if err != nil { + t.Errorf("couldn't read file %v: %v", tt.want, err) + } + if !reflect.DeepEqual(string(got), string(r1)) { + t.Errorf("GeneratePDBTemplate() = %v, want %v", string(got), string(r1)) + } + }) + } +} diff --git a/internal/templating/resources/pdb/test-resources/result-nginx.yaml b/internal/templating/resources/pdb/test-resources/result-nginx.yaml new file mode 100644 index 00000000..05197a88 --- /dev/null +++ b/internal/templating/resources/pdb/test-resources/result-nginx.yaml @@ -0,0 +1,29 @@ +--- +apiVersion: policy/v1 +kind: PodDisruptionBudget +metadata: + annotations: + lagoon.sh/branch: brancha + lagoon.sh/version: v2.x.x + creationTimestamp: null + labels: + app.kubernetes.io/instance: nginx + app.kubernetes.io/managed-by: build-deploy-tool + app.kubernetes.io/name: nginx-php-persistent + lagoon.sh/buildType: branch + lagoon.sh/environment: brancha + lagoon.sh/environmentType: production + lagoon.sh/project: example-project + lagoon.sh/service: nginx + lagoon.sh/service-type: nginx-php-persistent + name: nginx-pdb +spec: + minAvailable: 3 + selector: + matchLabels: + lagoon.sh/service: nginx +status: + currentHealthy: 0 + desiredHealthy: 0 + disruptionsAllowed: 0 + expectedPods: 0 diff --git a/legacy/build-deploy-docker-compose.sh b/legacy/build-deploy-docker-compose.sh index cec16b86..123a64f1 100755 --- a/legacy/build-deploy-docker-compose.sh +++ b/legacy/build-deploy-docker-compose.sh @@ -1612,6 +1612,8 @@ do done +build-deploy-tool template resource-workloads --saved-templates-path $YAML_FOLDER + set +x currentStepEnd="$(date +"%Y-%m-%d %H:%M:%S")" patchBuildStep "${buildStartTime}" "${previousStepEnd}" "${currentStepEnd}" "${NAMESPACE}" "deploymentTemplatingComplete" "Deployment Templating" diff --git a/test-resources/template-resources/test1-results/hpas.yaml b/test-resources/template-resources/test1-results/hpas.yaml new file mode 100644 index 00000000..e69de29b diff --git a/test-resources/template-resources/test1-results/pdbs.yaml b/test-resources/template-resources/test1-results/pdbs.yaml new file mode 100644 index 00000000..e69de29b diff --git a/test-resources/template-resources/test1/docker-compose.yml b/test-resources/template-resources/test1/docker-compose.yml new file mode 100644 index 00000000..85386270 --- /dev/null +++ b/test-resources/template-resources/test1/docker-compose.yml @@ -0,0 +1,20 @@ +version: '2' +services: + node: + networks: + - amazeeio-network + - default + build: + context: . + dockerfile: node.dockerfile + labels: + lagoon.type: node + volumes: + - .:/app:delegated + environment: + - LAGOON_LOCALDEV_HTTP_PORT=3000 + - LAGOON_ROUTE=http://node.docker.amazee.io + +networks: + amazeeio-network: + external: true \ No newline at end of file diff --git a/test-resources/template-resources/test1/lagoon.yml b/test-resources/template-resources/test1/lagoon.yml new file mode 100644 index 00000000..e4ed8d4d --- /dev/null +++ b/test-resources/template-resources/test1/lagoon.yml @@ -0,0 +1,10 @@ +docker-compose-yaml: ../test-resources/template-resources/test1/docker-compose.yml + +environment_variables: + git_sha: "true" + +environments: + main: + routes: + - node: + - example.com diff --git a/test-resources/template-resources/test2-results/hpas.yaml b/test-resources/template-resources/test2-results/hpas.yaml new file mode 100644 index 00000000..e07c216f --- /dev/null +++ b/test-resources/template-resources/test2-results/hpas.yaml @@ -0,0 +1,36 @@ +--- +apiVersion: autoscaling/v2 +kind: HorizontalPodAutoscaler +metadata: + annotations: + lagoon.sh/branch: main + lagoon.sh/version: v2.7.x + creationTimestamp: null + labels: + app.kubernetes.io/instance: node + app.kubernetes.io/managed-by: build-deploy-tool + app.kubernetes.io/name: node + lagoon.sh/buildType: branch + lagoon.sh/environment: main + lagoon.sh/environmentType: production + lagoon.sh/project: example-project + lagoon.sh/service: node + lagoon.sh/service-type: node + name: node-hpa +spec: + maxReplicas: 16 + metrics: + - resource: + name: cpu + target: + averageUtilization: 3000 + type: Utilization + type: Resource + minReplicas: 8 + scaleTargetRef: + apiVersion: apps/v1 + kind: Deployment + name: node +status: + currentMetrics: null + desiredReplicas: 0 diff --git a/test-resources/template-resources/test2-results/pdbs.yaml b/test-resources/template-resources/test2-results/pdbs.yaml new file mode 100644 index 00000000..e69de29b diff --git a/test-resources/template-resources/test2/docker-compose.yml b/test-resources/template-resources/test2/docker-compose.yml new file mode 100644 index 00000000..3f8ad451 --- /dev/null +++ b/test-resources/template-resources/test2/docker-compose.yml @@ -0,0 +1,21 @@ +version: '2' +services: + node: + networks: + - amazeeio-network + - default + build: + context: . + dockerfile: node.dockerfile + labels: + lagoon.type: node + lagoon.scalingparameter: node + volumes: + - .:/app:delegated + environment: + - LAGOON_LOCALDEV_HTTP_PORT=3000 + - LAGOON_ROUTE=http://node.docker.amazee.io + +networks: + amazeeio-network: + external: true \ No newline at end of file diff --git a/test-resources/template-resources/test2/lagoon.yml b/test-resources/template-resources/test2/lagoon.yml new file mode 100644 index 00000000..85296111 --- /dev/null +++ b/test-resources/template-resources/test2/lagoon.yml @@ -0,0 +1,10 @@ +docker-compose-yaml: ../test-resources/template-resources/test2/docker-compose.yml + +environment_variables: + git_sha: "true" + +environments: + main: + routes: + - node: + - example.com diff --git a/test-resources/template-resources/test2/resources.json b/test-resources/template-resources/test2/resources.json new file mode 100644 index 00000000..d82c8b5f --- /dev/null +++ b/test-resources/template-resources/test2/resources.json @@ -0,0 +1,23 @@ +{ + "node": { + "serviceType": "node", + "hpa": { + "spec": { + "minReplicas": 8, + "maxReplicas": 16, + "metrics": [ + { + "type": "Resource", + "resource": { + "name": "cpu", + "target": { + "type": "Utilization", + "averageUtilization": 3000 + } + } + } + ] + } + } + } +} \ No newline at end of file diff --git a/test-resources/template-resources/test3-results/hpas.yaml b/test-resources/template-resources/test3-results/hpas.yaml new file mode 100644 index 00000000..0a4e426a --- /dev/null +++ b/test-resources/template-resources/test3-results/hpas.yaml @@ -0,0 +1,36 @@ +--- +apiVersion: autoscaling/v2 +kind: HorizontalPodAutoscaler +metadata: + annotations: + lagoon.sh/branch: main + lagoon.sh/version: v2.7.x + creationTimestamp: null + labels: + app.kubernetes.io/instance: nginx-php + app.kubernetes.io/managed-by: build-deploy-tool + app.kubernetes.io/name: nginx-php-persistent + lagoon.sh/buildType: branch + lagoon.sh/environment: main + lagoon.sh/environmentType: production + lagoon.sh/project: example-project + lagoon.sh/service: nginx-php + lagoon.sh/service-type: nginx-php-persistent + name: nginx-php-hpa +spec: + maxReplicas: 16 + metrics: + - resource: + name: cpu + target: + averageUtilization: 3000 + type: Utilization + type: Resource + minReplicas: 8 + scaleTargetRef: + apiVersion: apps/v1 + kind: Deployment + name: nginx-php +status: + currentMetrics: null + desiredReplicas: 0 diff --git a/test-resources/template-resources/test3-results/pdbs.yaml b/test-resources/template-resources/test3-results/pdbs.yaml new file mode 100644 index 00000000..058edd44 --- /dev/null +++ b/test-resources/template-resources/test3-results/pdbs.yaml @@ -0,0 +1,29 @@ +--- +apiVersion: policy/v1 +kind: PodDisruptionBudget +metadata: + annotations: + lagoon.sh/branch: main + lagoon.sh/version: v2.7.x + creationTimestamp: null + labels: + app.kubernetes.io/instance: nginx-php + app.kubernetes.io/managed-by: build-deploy-tool + app.kubernetes.io/name: nginx-php-persistent + lagoon.sh/buildType: branch + lagoon.sh/environment: main + lagoon.sh/environmentType: production + lagoon.sh/project: example-project + lagoon.sh/service: nginx-php + lagoon.sh/service-type: nginx-php-persistent + name: nginx-php-pdb +spec: + minAvailable: 2 + selector: + matchLabels: + lagoon.sh/service: nginx-php +status: + currentHealthy: 0 + desiredHealthy: 0 + disruptionsAllowed: 0 + expectedPods: 0 diff --git a/test-resources/template-resources/test3/docker-compose.yml b/test-resources/template-resources/test3/docker-compose.yml new file mode 100644 index 00000000..06d54744 --- /dev/null +++ b/test-resources/template-resources/test3/docker-compose.yml @@ -0,0 +1,147 @@ +version: '2.3' + +x-example-image-version: + &example-image-version ${EXAMPLE_IMAGE_VERSION:-4.x} + +x-project: + &project ${PROJECT_NAME:-mysite} + +x-volumes: + &default-volumes + volumes: + - .:/app:${VOLUME_FLAGS:-delegated} ### Local overrides to mount host filesystem. Automatically removed in CI and PROD. + - ./docroot/sites/default/files:/app/docroot/sites/default/files:${VOLUME_FLAGS:-delegated} ### Local overrides to mount host filesystem. Automatically removed in CI and PROD. + +x-environment: + &default-environment + LAGOON_PROJECT: *project + DRUPAL_HASH_SALT: fakehashsaltfakehashsaltfakehashsalt + LAGOON_LOCALDEV_URL: ${LOCALDEV_URL:-http://mysite.docker.amazee.io} + LAGOON_ROUTE: ${LOCALDEV_URL:-http://mysite.docker.amazee.io} + GITHUB_TOKEN: ${GITHUB_TOKEN:-} + EXAMPLE_KEY: ${EXAMPLE_KEY:-} + EXAMPLE_IMAGE_VERSION: ${EXAMPLE_IMAGE_VERSION:-latest} + LAGOON_ENVIRONMENT_TYPE: ${LAGOON_ENVIRONMENT_TYPE:-local} + DRUPAL_REFRESH_SEARCHAPI: ${DRUPAL_REFRESH_SEARCHAPI:-} + EXAMPLE_INGRESS_PSK: ${EXAMPLE_INGRESS_PSK:-} + EXAMPLE_INGRESS_HEADER: ${EXAMPLE_INGRESS_HEADER:-} + EXAMPLE_INGRESS_ENABLED: ${EXAMPLE_INGRESS_ENABLED:-} + REDIS_CACHE_PREFIX: "tide_" + DB_ALIAS: ${DB_ALIAS:-bay.production} + + +services: + + cli: + build: + context: . + dockerfile: .docker/Dockerfile.cli + args: + COMPOSER: ${COMPOSER:-composer.json} + EXAMPLE_IMAGE_VERSION: *example-image-version + image: *project + environment: + << : *default-environment + << : *default-volumes + volumes_from: ### Local overrides to mount host SSH keys. Automatically removed in CI. + - container:amazeeio-ssh-agent ### Local overrides to mount host SSH keys. Automatically removed in CI. + labels: + lagoon.type: cli-persistent + lagoon.persistent: /app/docroot/sites/default/files/ + lagoon.persistent.name: nginx-php + lagoon.persistent.size: 5Gi + + nginx: + build: + context: . + dockerfile: .docker/Dockerfile.nginx-drupal + args: + CLI_IMAGE: *project + EXAMPLE_IMAGE_VERSION: *example-image-version + << : *default-volumes + environment: + << : *default-environment + depends_on: + - cli + networks: + - amazeeio-network + - default + labels: + lagoon.type: nginx-php-persistent + lagoon.persistent: /app/docroot/sites/default/files/ + lagoon.persistent.size: 5Gi + lagoon.name: nginx-php + lagoon.scalingparameter: nginx-php-performance + expose: + - "8080" + php: + build: + context: . + dockerfile: .docker/Dockerfile.php + args: + CLI_IMAGE: *project + EXAMPLE_IMAGE_VERSION: *example-image-version + environment: + << : *default-environment + << : *default-volumes + depends_on: + - cli + labels: + lagoon.type: nginx-php-persistent + lagoon.persistent: /app/docroot/sites/default/files/ + lagoon.persistent.size: 5Gi + lagoon.name: nginx-php + + mariadb: + image: amazeeio/mariadb-drupal + environment: + << : *default-environment + ports: + - "3306" # Find port on host with `ahoy info` or `docker-compose port mariadb 3306` + labels: + lagoon.type: mariadb-shared + + redis: + image: amazeeio/redis + labels: + lagoon.type: redis + + elasticsearch: + build: + context: . + dockerfile: .docker/Dockerfile.elasticsearch + args: + - ES_TPL=${ES_TPL:-elasticsearch.yml} + environment: + - discovery.type=single-node + labels: + lagoon.type: none + + chrome: + image: selenium/standalone-chrome:3.141.59-oxygen + shm_size: '1gb' + environment: + << : *default-environment + << : *default-volumes + depends_on: + - cli + labels: + lagoon.type: none + + clamav: + image: clamav/clamav:${EXAMPLE_IMAGE_VERSION:-4.x} + environment: + << : *default-environment + ports: + - "3310" + labels: + lagoon.type: none + + +networks: + amazeeio-network: + external: true + +volumes: + app: {} + files: {} \ No newline at end of file diff --git a/test-resources/template-resources/test3/lagoon.yml b/test-resources/template-resources/test3/lagoon.yml new file mode 100644 index 00000000..99b14e38 --- /dev/null +++ b/test-resources/template-resources/test3/lagoon.yml @@ -0,0 +1,43 @@ +--- +docker-compose-yaml: ../test-resources/template-resources/test3/docker-compose.yml + +project: content-example-com + +environments: + production: + cronjobs: + - name: drush cron + schedule: "*/15 * * * *" + command: 'drush cron' + service: cli + + routes: + - nginx-php: + - "content.example.com": + monitoring-path: "/api/v1" + tls-acme: 'false' + insecure: Allow + master: + cronjobs: + - name: drush cron + schedule: "0 1,4 * * *" + command: 'drush cron' + service: cli + + routes: + - nginx-php: + - "master.content.example.com": + tls-acme: 'false' + insecure: Allow + develop: + cronjobs: + - name: drush cron + schedule: "0 1,4 * * *" + command: 'drush cron' + service: cli + + routes: + - nginx-php: + - "develop.content.example.com": + tls-acme: 'false' + insecure: Allow diff --git a/test-resources/template-resources/test3/resources.json b/test-resources/template-resources/test3/resources.json new file mode 100644 index 00000000..705a99a2 --- /dev/null +++ b/test-resources/template-resources/test3/resources.json @@ -0,0 +1,28 @@ +{ + "nginx-php-performance": { + "serviceType": "nginx-php-persistent", + "hpa": { + "spec": { + "minReplicas": 8, + "maxReplicas": 16, + "metrics": [ + { + "type": "Resource", + "resource": { + "name": "cpu", + "target": { + "type": "Utilization", + "averageUtilization": 3000 + } + } + } + ] + } + }, + "pdb": { + "spec": { + "minAvailable": 2 + } + } + } +} \ No newline at end of file