diff --git a/install/installer/cmd/render.go b/install/installer/cmd/render.go index 75da3b8f255df8..92afd78eeaab91 100644 --- a/install/installer/cmd/render.go +++ b/install/installer/cmd/render.go @@ -149,6 +149,11 @@ func renderKubernetesObjects(cfgVersion string, cfg *configv1.Config) ([]string, fmt.Fprintln(os.Stderr, "configuration is invalid") os.Exit(1) } + + // Warnings are printed to stderr + for _, r := range res.Warnings { + fmt.Fprintf(os.Stderr, "%s\n", r) + } } ctx, err := common.NewRenderContext(*cfg, *versionMF, renderOpts.Namespace) diff --git a/install/installer/pkg/components/proxy/service.go b/install/installer/pkg/components/proxy/service.go index 8c3145a054f309..9cc2df9e52ccd0 100644 --- a/install/installer/pkg/components/proxy/service.go +++ b/install/installer/pkg/components/proxy/service.go @@ -22,7 +22,6 @@ var allowedServiceTypes = map[corev1.ServiceType]struct{}{ } func service(ctx *common.RenderContext) ([]runtime.Object, error) { - serviceType := corev1.ServiceTypeLoadBalancer loadBalancerIP := "" _ = ctx.WithExperimental(func(cfg *experimental.Config) error { @@ -30,17 +29,21 @@ func service(ctx *common.RenderContext) ([]runtime.Object, error) { if cfg.WebApp.ProxyConfig.StaticIP != "" { loadBalancerIP = cfg.WebApp.ProxyConfig.StaticIP } - st := cfg.WebApp.ProxyConfig.ServiceType - if st != nil { - _, allowed := allowedServiceTypes[corev1.ServiceType(*st)] - if allowed { - serviceType = *st - } - } } return nil }) + serviceType := corev1.ServiceTypeLoadBalancer + if ctx.Config.Components != nil && ctx.Config.Components.Proxy != nil && ctx.Config.Components.Proxy.Service != nil { + st := ctx.Config.Components.Proxy.Service.ServiceType + if st != nil { + _, allowed := allowedServiceTypes[corev1.ServiceType(*st)] + if allowed { + serviceType = *st + } + } + } + var annotations map[string]string _ = ctx.WithExperimental(func(cfg *experimental.Config) error { if cfg.WebApp != nil && cfg.WebApp.ProxyConfig != nil { @@ -78,10 +81,10 @@ func service(ctx *common.RenderContext) ([]runtime.Object, error) { service.Spec.Type = serviceType if serviceType == corev1.ServiceTypeLoadBalancer { service.Spec.LoadBalancerIP = loadBalancerIP - } - service.Annotations["external-dns.alpha.kubernetes.io/hostname"] = fmt.Sprintf("%s,*.%s,*.ws.%s", ctx.Config.Domain, ctx.Config.Domain, ctx.Config.Domain) - service.Annotations["cloud.google.com/neg"] = `{"exposed_ports": {"80":{},"443": {}}}` + service.Annotations["external-dns.alpha.kubernetes.io/hostname"] = fmt.Sprintf("%s,*.%s,*.ws.%s", ctx.Config.Domain, ctx.Config.Domain, ctx.Config.Domain) + service.Annotations["cloud.google.com/neg"] = `{"exposed_ports": {"80":{},"443": {}}}` + } for k, v := range annotations { service.Annotations[k] = v diff --git a/install/installer/pkg/components/proxy/service_test.go b/install/installer/pkg/components/proxy/service_test.go index 9b73a188449f53..8ef47721f28370 100644 --- a/install/installer/pkg/components/proxy/service_test.go +++ b/install/installer/pkg/components/proxy/service_test.go @@ -4,6 +4,7 @@ package proxy import ( + "fmt" "testing" "github.com/gitpod-io/gitpod/installer/pkg/common" @@ -12,11 +13,12 @@ import ( "github.com/gitpod-io/gitpod/installer/pkg/config/versions" "github.com/stretchr/testify/require" corev1 "k8s.io/api/core/v1" + "k8s.io/utils/pointer" ) func TestServiceLoadBalancerIP(t *testing.T) { const loadBalancerIP = "123.456.789.0" - ctx := renderContextWithProxyConfig(t, &experimental.ProxyConfig{StaticIP: loadBalancerIP}) + ctx := renderContextWithProxyConfig(t, &experimental.ProxyConfig{StaticIP: loadBalancerIP}, nil) objects, err := service(ctx) require.NoError(t, err) @@ -28,24 +30,98 @@ func TestServiceLoadBalancerIP(t *testing.T) { } func TestServiceAnnotations(t *testing.T) { - annotations := map[string]string{"hello": "world"} + testCases := []struct { + Name string + Annotations map[string]string + Components *config.Components + Expect func(ctx *common.RenderContext, svc *corev1.Service, annotations map[string]string) + }{ + { + Name: "Default to LoadBalancer", + Annotations: map[string]string{"hello": "world"}, + Expect: func(ctx *common.RenderContext, svc *corev1.Service, annotations map[string]string) { + // Check standard load balancer annotations + annotations = loadBalancerAnnotations(ctx, annotations) - ctx := renderContextWithProxyConfig(t, &experimental.ProxyConfig{ServiceAnnotations: annotations}) + for k, v := range annotations { + require.Equalf(t, annotations[k], svc.Annotations[k], + "expected to find annotation %q:%q on proxy service, but found %q:%q", k, v, k, svc.Annotations[k]) + } + }, + }, + { + Name: "Set to LoadBalancer", + Components: &config.Components{ + Proxy: &config.ProxyComponent{ + Service: &config.ComponentTypeService{ + ServiceType: (*corev1.ServiceType)(pointer.String(string(corev1.ServiceTypeLoadBalancer))), + }, + }, + }, + Annotations: map[string]string{"hello": "world", "hello2": "world2"}, + Expect: func(ctx *common.RenderContext, svc *corev1.Service, annotations map[string]string) { + // Check standard load balancer annotations + annotations = loadBalancerAnnotations(ctx, annotations) - objects, err := service(ctx) - require.NoError(t, err) + for k, v := range annotations { + require.Equalf(t, annotations[k], svc.Annotations[k], + "expected to find annotation %q:%q on proxy service, but found %q:%q", k, v, k, svc.Annotations[k]) + } + }, + }, + { + Name: "Set to ClusterIP", + Components: &config.Components{ + Proxy: &config.ProxyComponent{ + Service: &config.ComponentTypeService{ + ServiceType: (*corev1.ServiceType)(pointer.String(string(corev1.ServiceTypeClusterIP))), + }, + }, + }, + Annotations: map[string]string{"hello": "world"}, + Expect: func(ctx *common.RenderContext, svc *corev1.Service, annotations map[string]string) { + // Check standard load balancer annotations not present + lbAnnotations := loadBalancerAnnotations(ctx, make(map[string]string, 0)) - require.Len(t, objects, 1, "must render only one object") + for k := range lbAnnotations { + require.NotContains(t, annotations, k) + } - svc := objects[0].(*corev1.Service) - for k, v := range annotations { - require.Equalf(t, annotations[k], svc.Annotations[k], - "expected to find annotation %q:%q on proxy service, but found %q:%q", k, v, k, svc.Annotations[k]) + for k, v := range annotations { + require.Equalf(t, annotations[k], svc.Annotations[k], + "expected to find annotation %q:%q on proxy service, but found %q:%q", k, v, k, svc.Annotations[k]) + } + }, + }, + } + + for _, testCase := range testCases { + t.Run(testCase.Name, func(t *testing.T) { + ctx := renderContextWithProxyConfig(t, &experimental.ProxyConfig{ServiceAnnotations: testCase.Annotations}, testCase.Components) + + objects, err := service(ctx) + require.NoError(t, err) + + require.Len(t, objects, 1, "must render only one object") + + svc := objects[0].(*corev1.Service) + + testCase.Expect(ctx, svc, testCase.Annotations) + }) } } -func renderContextWithProxyConfig(t *testing.T, proxyConfig *experimental.ProxyConfig) *common.RenderContext { +func loadBalancerAnnotations(ctx *common.RenderContext, annotations map[string]string) map[string]string { + annotations["external-dns.alpha.kubernetes.io/hostname"] = fmt.Sprintf("%s,*.%s,*.ws.%s", ctx.Config.Domain, ctx.Config.Domain, ctx.Config.Domain) + annotations["cloud.google.com/neg"] = `{"exposed_ports": {"80":{},"443": {}}}` + + return annotations +} + +func renderContextWithProxyConfig(t *testing.T, proxyConfig *experimental.ProxyConfig, components *config.Components) *common.RenderContext { ctx, err := common.NewRenderContext(config.Config{ + Domain: "some-domain", + Components: components, Experimental: &experimental.Config{ WebApp: &experimental.WebAppConfig{ ProxyConfig: proxyConfig, diff --git a/install/installer/pkg/config/loader.go b/install/installer/pkg/config/loader.go index ccc88bc711f651..e15f931fbdaf17 100644 --- a/install/installer/pkg/config/loader.go +++ b/install/installer/pkg/config/loader.go @@ -47,6 +47,10 @@ type ConfigVersion interface { // ClusterValidation introduces configuration specific cluster validation checks ClusterValidation(cfg interface{}) cluster.ValidationChecks + + // CheckDeprecated checks for deprecated config params. + // Returns key/value pair of deprecated params/values and any error messages (used for conflicting params) + CheckDeprecated(cfg interface{}) (map[string]interface{}, []string) } // AddVersion adds a new version. diff --git a/install/installer/pkg/config/v1/config.go b/install/installer/pkg/config/v1/config.go index 9d3ab829ea2334..b52645d609f37f 100644 --- a/install/installer/pkg/config/v1/config.go +++ b/install/installer/pkg/config/v1/config.go @@ -73,6 +73,34 @@ func (v version) Defaults(in interface{}) error { return nil } +func (v version) CheckDeprecated(rawCfg interface{}) (map[string]interface{}, []string) { + warnings := make(map[string]interface{}, 0) + conflicts := make([]string, 0) + cfg := rawCfg.(*Config) + + if cfg.Experimental != nil && cfg.Experimental.WebApp != nil && cfg.Experimental.WebApp.ProxyConfig != nil && cfg.Experimental.WebApp.ProxyConfig.ServiceType != nil { + warnings["experimental.webapp.proxy.serviceType"] = *cfg.Experimental.WebApp.ProxyConfig.ServiceType + + if cfg.Components != nil && cfg.Components.Proxy != nil && cfg.Components.Proxy.Service != nil && cfg.Components.Proxy.Service.ServiceType != nil { + conflicts = append(conflicts, "Cannot set proxy service type in both components and experimental") + } else { + // Promote the experimental value to the components + if cfg.Components == nil { + cfg.Components = &Components{} + } + if cfg.Components.Proxy == nil { + cfg.Components.Proxy = &ProxyComponent{} + } + if cfg.Components.Proxy.Service == nil { + cfg.Components.Proxy.Service = &ComponentTypeService{} + } + cfg.Components.Proxy.Service.ServiceType = cfg.Experimental.WebApp.ProxyConfig.ServiceType + } + } + + return warnings, conflicts +} + // Config defines the v1 version structure of the gitpod config file type Config struct { // Installation type to run - for most users, this will be Full @@ -113,6 +141,8 @@ type Config struct { Customization *[]Customization `json:"customization,omitempty"` + Components *Components `json:"components,omitempty"` + Experimental *experimental.Config `json:"experimental,omitempty"` } @@ -340,3 +370,15 @@ type Customization struct { type CustomizationSpec struct { Env []corev1.EnvVar `json:"env"` } + +type Components struct { + Proxy *ProxyComponent `json:"proxy,omitempty"` +} + +type ProxyComponent struct { + Service *ComponentTypeService `json:"service,omitempty"` +} + +type ComponentTypeService struct { + ServiceType *corev1.ServiceType `json:"serviceType,omitempty" validate:"omitempty,service_config_type"` +} diff --git a/install/installer/pkg/config/v1/experimental/experimental.go b/install/installer/pkg/config/v1/experimental/experimental.go index fda74cc5c74a87..b1dd94f1c8c6e2 100644 --- a/install/installer/pkg/config/v1/experimental/experimental.go +++ b/install/installer/pkg/config/v1/experimental/experimental.go @@ -164,9 +164,11 @@ type BlockedRepository struct { } type ProxyConfig struct { - StaticIP string `json:"staticIP"` - ServiceAnnotations map[string]string `json:"serviceAnnotations"` - ServiceType *corev1.ServiceType `json:"serviceType,omitempty" validate:"omitempty,service_config_type"` + StaticIP string `json:"staticIP"` + ServiceAnnotations map[string]string `json:"serviceAnnotations"` + + // @deprecated use components.proxy.service.serviceType instead + ServiceType *corev1.ServiceType `json:"serviceType,omitempty" validate:"omitempty,service_config_type"` } type PublicAPIConfig struct { diff --git a/install/installer/pkg/config/validation.go b/install/installer/pkg/config/validation.go index 40f36bd831192c..dad71af36cb981 100644 --- a/install/installer/pkg/config/validation.go +++ b/install/installer/pkg/config/validation.go @@ -33,6 +33,14 @@ func Validate(version ConfigVersion, cfg interface{}) (r *ValidationResult, err } var res ValidationResult + + warnings, conflicts := version.CheckDeprecated(cfg) + + for k, v := range warnings { + res.Warnings = append(res.Warnings, fmt.Sprintf("Deprecated config parameter: %s=%v", k, v)) + } + res.Fatal = append(res.Fatal, conflicts...) + err = validate.Struct(cfg) if err != nil { validationErrors := err.(validator.ValidationErrors) diff --git a/install/kots/manifests/gitpod-installation-status.yaml b/install/kots/manifests/gitpod-installation-status.yaml index e965f8ab3d04bb..adc1a7ecd625a3 100644 --- a/install/kots/manifests/gitpod-installation-status.yaml +++ b/install/kots/manifests/gitpod-installation-status.yaml @@ -30,7 +30,7 @@ spec: containers: - name: installation-status # This will normally be the release tag - image: "eu.gcr.io/gitpod-core-dev/build/installer:tar-preview-telemetry.25" + image: "eu.gcr.io/gitpod-core-dev/build/installer:sje-installer-clusterip.8" command: - /bin/sh - -c diff --git a/install/kots/manifests/gitpod-installer-job.yaml b/install/kots/manifests/gitpod-installer-job.yaml index d2b6f8b5cfeffb..9a190a06711d1d 100644 --- a/install/kots/manifests/gitpod-installer-job.yaml +++ b/install/kots/manifests/gitpod-installer-job.yaml @@ -28,7 +28,7 @@ spec: containers: - name: installer # This will normally be the release tag - image: "eu.gcr.io/gitpod-core-dev/build/installer:tar-preview-telemetry.25" + image: "eu.gcr.io/gitpod-core-dev/build/installer:sje-installer-clusterip.8" volumeMounts: - mountPath: /config-patch name: config-patch @@ -263,6 +263,15 @@ spec: if [ '{{repl ConfigOptionEquals "advanced_mode_enabled" "1" }}' = "true" ]; then + echo "Gitpod: Applying advanced configuration" + + if [ '{{repl ConfigOptionNotEquals "component_proxy_service_serviceType" "" }}' = "true" ]; + then + # Empty string defaults to LoadBalancer. This maintains backwards compatibility with the deprecated experimental value + echo "Gitpod: Applying Proxy service type" + yq e -i ".components.proxy.service.serviceType = \"{{repl ConfigOption "component_proxy_service_serviceType" }}\"" "${CONFIG_FILE}" + fi + if [ '{{repl ConfigOptionNotEquals "customization_patch" "" }}' = "true" ]; then CUSTOMIZATION='{{repl ConfigOptionData "customization_patch" | Base64Encode }}' @@ -271,6 +280,8 @@ spec: # Apply the customization property - if something else is set, this will be ignored yq e -i ".customization = $(echo "${CUSTOMIZATION}" | base64 -d | yq e -o json '.customization' - | jq -rc) // []" "${CONFIG_FILE}" fi + else + echo "Gitpod: No advanced configuration applied" fi echo "Gitpod: Update platform telemetry value" diff --git a/install/kots/manifests/kots-config.yaml b/install/kots/manifests/kots-config.yaml index 0eeae85e9f0206..96e081f257827c 100644 --- a/install/kots/manifests/kots-config.yaml +++ b/install/kots/manifests/kots-config.yaml @@ -370,14 +370,14 @@ spec: Add the domain only (eg, `gitpod.io`). Separate multiple domains with spaces. - name: advanced - title: Additional Options - description: Here are additional options that you should only make use of in coordination with us or when you know what you are doing. + title: Advanced Options + description: Here are advanced options that you should only make use of in coordination with us or when you know what you are doing. items: - name: advanced_mode_enabled - title: Enable additional options + title: Enable advanced options type: bool default: "0" - help_text: Enables additional customization options. Enable only when you know what you are doing! + help_text: Enables advanced customization options. Enable only when you know what you are doing! - name: customization_patch title: Gitpod customization patch (YAML file) @@ -400,3 +400,24 @@ spec: required: false when: '{{repl ConfigOptionEquals "advanced_mode_enabled" "1" }}' help_text: A file with Gitpod config that will be used to patch the generated Gitpod config. Usually provided by Gitpod as a way to tailor your installation. + + - name: components + title: Components + description: Customize your component configuration + when: '{{repl ConfigOptionEquals "advanced_mode_enabled" "1" }}' + items: + - name: component_proxy_service_serviceType + title: Proxy service type + default: "" + help_text: | + Select the [Service Type](https://kubernetes.io/docs/concepts/services-networking/service/#publishing-services-service-types) + for the Proxy service. If using anything other than "Load Balancer", you are responsible for configuring your network to route + traffic through to the `proxy` service. + type: select_one + items: + - name: "" + title: Load balancer + - name: ClusterIP + title: Cluster IP + - name: NodePort + title: Node port