diff --git a/components/licensor/ee/cmd/validate.go b/components/licensor/ee/cmd/validate.go index ccc2d54b906c8d..77338866052b99 100644 --- a/components/licensor/ee/cmd/validate.go +++ b/components/licensor/ee/cmd/validate.go @@ -22,18 +22,28 @@ var validateCmd = &cobra.Command{ Short: "Validates a license - reads from stdin if no argument is provided", Args: cobra.MaximumNArgs(1), RunE: func(cmd *cobra.Command, args []string) (err error) { - var lic []byte - if len(args) == 0 { - lic, err = io.ReadAll(os.Stdin) - if err != nil { - return err + domain, _ := cmd.Flags().GetString("domain") + licensorType, _ := cmd.Flags().GetString("licensor") + + var e licensor.Evaluator + switch licensorType { + case string(licensor.LicenseTypeReplicated): + e = licensor.NewReplicatedEvaluator(domain) + break + default: + var lic []byte + if len(args) == 0 { + lic, err = io.ReadAll(os.Stdin) + if err != nil { + return err + } + } else { + lic = []byte(args[0]) } - } else { - lic = []byte(args[0]) + + e = licensor.NewGitpodEvaluator(lic, domain) } - domain, _ := cmd.Flags().GetString("domain") - e := licensor.NewEvaluator(lic, domain) if msg, valid := e.Validate(); !valid { return xerrors.Errorf(msg) } @@ -47,4 +57,5 @@ var validateCmd = &cobra.Command{ func init() { rootCmd.AddCommand(validateCmd) validateCmd.Flags().String("domain", "", "domain to evaluate the license against") + validateCmd.Flags().String("licensor", "gitpod", "licensor to use") } diff --git a/components/licensor/ee/pkg/licensor/gitpod.go b/components/licensor/ee/pkg/licensor/gitpod.go new file mode 100644 index 00000000000000..159e411d3a1256 --- /dev/null +++ b/components/licensor/ee/pkg/licensor/gitpod.go @@ -0,0 +1,108 @@ +// Copyright (c) 2022 Gitpod GmbH. All rights reserved. +// Licensed under the Gitpod Enterprise Source Code License, +// See License.enterprise.txt in the project root folder. + +package licensor + +import ( + "crypto" + "crypto/rsa" + "crypto/sha256" + "encoding/base64" + "encoding/json" + "fmt" + "time" +) + +// GitpodEvaluator determines what a license allows for +type GitpodEvaluator struct { + invalid string + lic LicensePayload +} + +// Validate returns false if the license isn't valid and a message explaining why that is. +func (e *GitpodEvaluator) Validate() (msg string, valid bool) { + if e.invalid == "" { + return "", true + } + + return e.invalid, false +} + +// Enabled determines if a feature is enabled by the license +func (e *GitpodEvaluator) Enabled(feature Feature) bool { + if e.invalid != "" { + return false + } + + _, ok := e.lic.Level.allowance().Features[feature] + return ok +} + +// HasEnoughSeats returns true if the license supports at least the give amount of seats +func (e *GitpodEvaluator) HasEnoughSeats(seats int) bool { + if e.invalid != "" { + return false + } + + return e.lic.Seats == 0 || seats <= e.lic.Seats +} + +// Inspect returns the license information this evaluator holds. +// This function is intended for transparency/debugging purposes only and must +// never be used to determine feature eligibility under a license. All code making +// those kinds of decisions must be part of the Evaluator. +func (e *GitpodEvaluator) Inspect() LicensePayload { + return e.lic +} + +// NewGitpodEvaluator produces a new license evaluator from a license key +func NewGitpodEvaluator(key []byte, domain string) (res *GitpodEvaluator) { + if len(key) == 0 { + // fallback to the default license + return &GitpodEvaluator{ + lic: defaultLicense, + } + } + + deckey := make([]byte, base64.StdEncoding.DecodedLen(len(key))) + n, err := base64.StdEncoding.Decode(deckey, key) + if err != nil { + return &GitpodEvaluator{invalid: fmt.Sprintf("cannot decode key: %q", err)} + } + deckey = deckey[:n] + + var lic licensePayload + err = json.Unmarshal(deckey, &lic) + if err != nil { + return &GitpodEvaluator{invalid: fmt.Sprintf("cannot unmarshal key: %q", err)} + } + + keyWoSig, err := json.Marshal(lic.LicensePayload) + if err != nil { + return &GitpodEvaluator{invalid: fmt.Sprintf("cannot remarshal key: %q", err)} + } + hashed := sha256.Sum256(keyWoSig) + + for _, k := range publicKeys { + err = rsa.VerifyPKCS1v15(k, crypto.SHA256, hashed[:], lic.Signature) + if err == nil { + break + } + } + if err != nil { + return &GitpodEvaluator{invalid: fmt.Sprintf("cannot verify key: %q", err)} + } + + if !matchesDomain(lic.Domain, domain) { + return &GitpodEvaluator{invalid: "wrong domain"} + } + + if lic.ValidUntil.Before(time.Now()) { + return &GitpodEvaluator{invalid: "not valid anymore"} + } + + return &GitpodEvaluator{ + lic: lic.LicensePayload, + } +} diff --git a/components/licensor/ee/pkg/licensor/licensor.go b/components/licensor/ee/pkg/licensor/licensor.go index 3bb5374f02ffe5..468088f6a5dd8c 100644 --- a/components/licensor/ee/pkg/licensor/licensor.go +++ b/components/licensor/ee/pkg/licensor/licensor.go @@ -17,6 +17,13 @@ import ( "time" ) +type LicenseType string + +const ( + LicenseTypeGitpod LicenseType = "gitpod" + LicenseTypeReplicated LicenseType = "replicated" +) + // LicensePayload is the actual license content type LicensePayload struct { ID string `json:"id"` @@ -115,57 +122,6 @@ var defaultLicense = LicensePayload{ // Domain, ValidUntil are free for all } -// NewEvaluator produces a new license evaluator from a license key -func NewEvaluator(key []byte, domain string) (res *Evaluator) { - if len(key) == 0 { - // fallback to the default license - return &Evaluator{ - lic: defaultLicense, - } - } - - deckey := make([]byte, base64.StdEncoding.DecodedLen(len(key))) - n, err := base64.StdEncoding.Decode(deckey, key) - if err != nil { - return &Evaluator{invalid: fmt.Sprintf("cannot decode key: %q", err)} - } - deckey = deckey[:n] - - var lic licensePayload - err = json.Unmarshal(deckey, &lic) - if err != nil { - return &Evaluator{invalid: fmt.Sprintf("cannot unmarshal key: %q", err)} - } - - keyWoSig, err := json.Marshal(lic.LicensePayload) - if err != nil { - return &Evaluator{invalid: fmt.Sprintf("cannot remarshal key: %q", err)} - } - hashed := sha256.Sum256(keyWoSig) - - for _, k := range publicKeys { - err = rsa.VerifyPKCS1v15(k, crypto.SHA256, hashed[:], lic.Signature) - if err == nil { - break - } - } - if err != nil { - return &Evaluator{invalid: fmt.Sprintf("cannot verify key: %q", err)} - } - - if !matchesDomain(lic.Domain, domain) { - return &Evaluator{invalid: "wrong domain"} - } - - if lic.ValidUntil.Before(time.Now()) { - return &Evaluator{invalid: "not valid anymore"} - } - - return &Evaluator{ - lic: lic.LicensePayload, - } -} - func matchesDomain(pattern, domain string) bool { if pattern == "" { return true @@ -184,46 +140,11 @@ func matchesDomain(pattern, domain string) bool { return false } -// Evaluator determines what a license allows for -type Evaluator struct { - invalid string - lic LicensePayload -} - -// Validate returns false if the license isn't valid and a message explaining why that is. -func (e *Evaluator) Validate() (msg string, valid bool) { - if e.invalid == "" { - return "", true - } - - return e.invalid, false -} - -// Enabled determines if a feature is enabled by the license -func (e *Evaluator) Enabled(feature Feature) bool { - if e.invalid != "" { - return false - } - - _, ok := e.lic.Level.allowance().Features[feature] - return ok -} - -// HasEnoughSeats returns true if the license supports at least the give amount of seats -func (e *Evaluator) HasEnoughSeats(seats int) bool { - if e.invalid != "" { - return false - } - - return e.lic.Seats == 0 || seats <= e.lic.Seats -} - -// Inspect returns the license information this evaluator holds. -// This function is intended for transparency/debugging purposes only and must -// never be used to determine feature eligibility under a license. All code making -// those kinds of decisions must be part of the Evaluator. -func (e *Evaluator) Inspect() LicensePayload { - return e.lic +type Evaluator interface { + Enabled(feature Feature) bool + HasEnoughSeats(seats int) bool + Inspect() LicensePayload + Validate() (msg string, valid bool) } // Sign signs a license so that it can be used with the evaluator diff --git a/components/licensor/ee/pkg/licensor/licensor_test.go b/components/licensor/ee/pkg/licensor/licensor_test.go index 85dfc8161ed27a..3521f1b27854fa 100644 --- a/components/licensor/ee/pkg/licensor/licensor_test.go +++ b/components/licensor/ee/pkg/licensor/licensor_test.go @@ -5,41 +5,135 @@ package licensor import ( + "bytes" "crypto/rand" "crypto/rsa" + "encoding/json" + "io/ioutil" + "net/http" "testing" "time" ) const ( - seats = 5 - domain = "foobar.com" - someID = "730d5134-768c-4a05-b7cd-ecf3757cada9" + seats = 5 + domain = "foobar.com" + someID = "730d5134-768c-4a05-b7cd-ecf3757cada9" + replicatedLicenseUrl = "http://kotsadm:3000/license/v1/license" ) type licenseTest struct { - Name string - License *LicensePayload - Validate func(t *testing.T, eval *Evaluator) + Name string + License *LicensePayload + Validate func(t *testing.T, eval Evaluator) + Type LicenseType + NeverExpires bool +} + +// roundTripFunc . +type roundTripFunc func(req *http.Request) *http.Response + +// roundTrip . +func (f roundTripFunc) RoundTrip(req *http.Request) (*http.Response, error) { + return f(req), nil +} + +// newTestClient returns *http.Client with Transport replaced to avoid making real calls +func newTestClient(fn roundTripFunc) *http.Client { + return &http.Client{ + Transport: roundTripFunc(fn), + } } func (test *licenseTest) Run(t *testing.T) { t.Run(test.Name, func(t *testing.T) { - var eval *Evaluator - if test.License == nil { - eval = NewEvaluator(nil, "") - } else { - priv, err := rsa.GenerateKey(rand.Reader, 2048) - if err != nil { - t.Fatalf("cannot generate key: %q", err) + var eval Evaluator + if test.Type == LicenseTypeGitpod { + if test.NeverExpires { + t.Fatal("gitpod licenses must have an expiry date") } - publicKeys = []*rsa.PublicKey{&priv.PublicKey} - lic, err := Sign(*test.License, priv) - if err != nil { - t.Fatalf("cannot sign license: %q", err) + + if test.License == nil { + eval = NewGitpodEvaluator(nil, "") + } else { + priv, err := rsa.GenerateKey(rand.Reader, 2048) + if err != nil { + t.Fatalf("cannot generate key: %q", err) + } + publicKeys = []*rsa.PublicKey{&priv.PublicKey} + lic, err := Sign(*test.License, priv) + if err != nil { + t.Fatalf("cannot sign license: %q", err) + } + + eval = NewGitpodEvaluator(lic, domain) } + } else if test.Type == LicenseTypeReplicated { + client := newTestClient(func(req *http.Request) *http.Response { + act := req.URL.String() + if act != "http://kotsadm:3000/license/v1/license" { + t.Fatalf("invalid kotsadm url match: expected %s, got %v", replicatedLicenseUrl, act) + } - eval = NewEvaluator(lic, domain) + payload, err := json.Marshal(replicatedLicensePayload{ + ExpirationTime: func() *time.Time { + if test.License != nil { + return &test.License.ValidUntil + } + if !test.NeverExpires { + t := time.Now().Add(-6 * time.Hour) + return &t + } + return nil + }(), + Fields: []replicatedFields{ + { + Field: "domain", + Value: func() string { + if test.License != nil { + return test.License.Domain + } + return domain + }(), + }, + { + Field: "levelId", + Value: func() LicenseLevel { + if test.License != nil { + return test.License.Level + } + return LevelTeam + }(), + }, + { + Field: "seats", + Value: func() int { + if test.License != nil { + return test.License.Seats + } + return seats + }(), + }, + }, + }) + if err != nil { + t.Fatalf("failed to convert payload: %v", err) + } + + return &http.Response{ + StatusCode: 200, + Body: ioutil.NopCloser(bytes.NewBuffer(payload)), + Header: make(http.Header), + } + }) + + if test.License == nil { + eval = newReplicatedEvaluator(client, domain) + } else { + eval = newReplicatedEvaluator(client, test.License.Domain) + } + } else { + t.Fatalf("unknown license type: '%s'", test.Type) } test.Validate(t, eval) @@ -54,16 +148,36 @@ func TestSeats(t *testing.T) { WithinLimits bool DefaultLicense bool InvalidLicense bool + LicenseType LicenseType + NeverExpires bool }{ - {"unlimited seats", 0, 1000, true, false, false}, - {"within limited seats", 50, 40, true, false, false}, - {"within limited seats (edge)", 50, 50, true, false, false}, - {"beyond limited seats", 50, 150, false, false, false}, - {"beyond limited seats (edge)", 50, 51, false, false, false}, - {"invalid license", 50, 50, false, false, true}, - {"within default license seats", 0, 7, true, true, false}, - {"within default license seats (edge)", 0, 10, true, true, false}, - {"beyond default license seats", 0, 11, false, true, false}, + {"Gitpod: unlimited seats", 0, 1000, true, false, false, LicenseTypeGitpod, false}, + {"Gitpod: within limited seats", 50, 40, true, false, false, LicenseTypeGitpod, false}, + {"Gitpod: within limited seats (edge)", 50, 50, true, false, false, LicenseTypeGitpod, false}, + {"Gitpod: beyond limited seats", 50, 150, false, false, false, LicenseTypeGitpod, false}, + {"Gitpod: beyond limited seats (edge)", 50, 51, false, false, false, LicenseTypeGitpod, false}, + {"Gitpod: invalid license", 50, 50, false, false, true, LicenseTypeGitpod, false}, + {"Gitpod: within default license seats", 0, 7, true, true, false, LicenseTypeGitpod, false}, + {"Gitpod: within default license seats (edge)", 0, 10, true, true, false, LicenseTypeGitpod, false}, + {"Gitpod: beyond default license seats", 0, 11, false, true, false, LicenseTypeGitpod, false}, + + // correctly missing the default license tests as Replicated always has a license + {"Replicated: unlimited seats", 0, 1000, true, false, false, LicenseTypeReplicated, false}, + {"Replicated: within limited seats", 50, 40, true, false, false, LicenseTypeReplicated, false}, + {"Replicated: within limited seats (edge)", 50, 50, true, false, false, LicenseTypeReplicated, false}, + {"Replicated: beyond limited seats", 50, 150, false, false, false, LicenseTypeReplicated, false}, + {"Replicated: beyond limited seats (edge)", 50, 51, false, false, false, LicenseTypeReplicated, false}, + {"Replicated: invalid license", 50, 50, false, false, true, LicenseTypeReplicated, false}, + {"Replicated: beyond default license seats", 0, 11, false, true, false, LicenseTypeReplicated, false}, + + {"Replicated: unlimited seats", 0, 1000, true, false, false, LicenseTypeReplicated, true}, + {"Replicated: within limited seats", 50, 40, true, false, false, LicenseTypeReplicated, true}, + {"Replicated: within limited seats (edge)", 50, 50, true, false, false, LicenseTypeReplicated, true}, + {"Replicated: beyond limited seats", 50, 150, false, false, false, LicenseTypeReplicated, true}, + {"Replicated: beyond limited seats (edge)", 50, 51, false, false, false, LicenseTypeReplicated, true}, + {"Replicated: invalid license", 50, 50, false, false, true, LicenseTypeReplicated, true}, + {"Replicated: beyond default license seats", 0, 11, false, true, false, LicenseTypeReplicated, true}, + {"Replicated: invalid license within default seats", 50, 5, true, false, true, LicenseTypeReplicated, false}, } for _, test := range tests { @@ -81,12 +195,14 @@ func TestSeats(t *testing.T) { Seats: test.Licensed, ValidUntil: validUntil, }, - Validate: func(t *testing.T, eval *Evaluator) { + Validate: func(t *testing.T, eval Evaluator) { withinLimits := eval.HasEnoughSeats(test.Probe) if withinLimits != test.WithinLimits { t.Errorf("HasEnoughSeats did not behave as expected: lic=%d probe=%d expected=%v actual=%v", test.Licensed, test.Probe, test.WithinLimits, withinLimits) } }, + Type: test.LicenseType, + NeverExpires: test.NeverExpires, } if test.DefaultLicense { lt.License = nil @@ -101,16 +217,26 @@ func TestFeatures(t *testing.T) { DefaultLicense bool Level LicenseLevel Features []Feature + LicenseType LicenseType }{ - {"no license", true, LicenseLevel(0), []Feature{FeaturePrebuild}}, - {"invalid license level", false, LicenseLevel(666), []Feature{}}, - {"enterprise license", false, LevelEnterprise, []Feature{ + {"Gitpod: no license", true, LicenseLevel(0), []Feature{FeaturePrebuild}, LicenseTypeGitpod}, + {"Gitpod: invalid license level", false, LicenseLevel(666), []Feature{}, LicenseTypeGitpod}, + {"Gitpod: enterprise license", false, LevelEnterprise, []Feature{ + FeatureAdminDashboard, + FeatureSetTimeout, + FeatureWorkspaceSharing, + FeatureSnapshot, + FeaturePrebuild, + }, LicenseTypeGitpod}, + + {"Replicated: invalid license level", false, LicenseLevel(666), []Feature{}, LicenseTypeReplicated}, + {"Replicated: enterprise license", false, LevelEnterprise, []Feature{ FeatureAdminDashboard, FeatureSetTimeout, FeatureWorkspaceSharing, FeatureSnapshot, FeaturePrebuild, - }}, + }, LicenseTypeReplicated}, } for _, test := range tests { @@ -127,7 +253,7 @@ func TestFeatures(t *testing.T) { lt := licenseTest{ Name: test.Name, License: lic, - Validate: func(t *testing.T, eval *Evaluator) { + Validate: func(t *testing.T, eval Evaluator) { unavailableFeatures := featureSet{} for f := range allowanceMap[LevelEnterprise].Features { unavailableFeatures[f] = struct{}{} @@ -146,6 +272,7 @@ func TestFeatures(t *testing.T) { } } }, + Type: test.LicenseType, } lt.Run(t) } @@ -258,7 +385,7 @@ func TestEvalutorKeys(t *testing.T) { } var errmsg string - e := NewEvaluator(lic, dom) + e := NewGitpodEvaluator(lic, dom) if msg, valid := e.Validate(); !valid { errmsg = msg } diff --git a/components/licensor/ee/pkg/licensor/replicated.go b/components/licensor/ee/pkg/licensor/replicated.go new file mode 100644 index 00000000000000..dca77210c64436 --- /dev/null +++ b/components/licensor/ee/pkg/licensor/replicated.go @@ -0,0 +1,130 @@ +// Copyright (c) 2022 Gitpod GmbH. All rights reserved. +// Licensed under the Gitpod Enterprise Source Code License, +// See License.enterprise.txt in the project root folder. + +package licensor + +import ( + "encoding/json" + "fmt" + "net/http" + "time" +) + +const ( + replicatedLicenseApiEndpoint = "http://kotsadm:3000/license/v1/license" + replicatedLicenseApiTimeout = 5 * time.Second +) + +type replicatedFields struct { + Field string `json:"field"` + Title string `json:"title"` + Type string `json:"type"` + Value interface{} `json:"value"` // This is of type "fieldType" +} + +// replicatedLicensePayload exists to convert the JSON structure to a LicensePayload +type replicatedLicensePayload struct { + LicenseID string `json:"license_id"` + InstallationID string `json:"installation_id"` + Assignee string `json:"assignee"` + ReleaseChannel string `json:"release_channel"` + LicenseType string `json:"license_type"` + ExpirationTime *time.Time `json:"expiration_time,omitempty"` // Not set if license never expires + Fields []replicatedFields `json:"fields"` +} + +type ReplicatedEvaluator struct { + invalid string + lic LicensePayload +} + +func (e *ReplicatedEvaluator) Enabled(feature Feature) bool { + if e.invalid != "" { + return false + } + + _, ok := e.lic.Level.allowance().Features[feature] + return ok +} + +func (e *ReplicatedEvaluator) HasEnoughSeats(seats int) bool { + if e.invalid != "" { + return false + } + + return e.lic.Seats == 0 || seats <= e.lic.Seats +} + +func (e *ReplicatedEvaluator) Inspect() LicensePayload { + return e.lic +} + +func (e *ReplicatedEvaluator) Validate() (msg string, valid bool) { + if e.invalid == "" { + return "", true + } + + return e.invalid, false +} + +// defaultReplicatedLicense this is the default license if call fails +func defaultReplicatedLicense() *ReplicatedEvaluator { + return &ReplicatedEvaluator{ + lic: defaultLicense, + } +} + +// newReplicatedEvaluator exists to allow mocking of client +func newReplicatedEvaluator(client *http.Client, domain string) (res *ReplicatedEvaluator) { + resp, err := client.Get(replicatedLicenseApiEndpoint) + if err != nil { + return &ReplicatedEvaluator{invalid: fmt.Sprintf("cannot query kots admin, %q", err)} + } + defer resp.Body.Close() + + var replicatedPayload replicatedLicensePayload + err = json.NewDecoder(resp.Body).Decode(&replicatedPayload) + if err != nil { + return &ReplicatedEvaluator{invalid: fmt.Sprintf("cannot decode json data, %q", err)} + } + + lic := LicensePayload{ + ID: replicatedPayload.LicenseID, + } + + // Search for the fields + for _, i := range replicatedPayload.Fields { + switch i.Field { + case "domain": + lic.Domain = i.Value.(string) + + case "levelId": + lic.Level = LicenseLevel(i.Value.(float64)) + + case "seats": + lic.Seats = int(i.Value.(float64)) + } + } + + if !matchesDomain(lic.Domain, domain) { + return defaultReplicatedLicense() + } + + if replicatedPayload.ExpirationTime != nil { + lic.ValidUntil = *replicatedPayload.ExpirationTime + + if lic.ValidUntil.Before(time.Now()) { + return defaultReplicatedLicense() + } + } + + return &ReplicatedEvaluator{ + lic: lic, + } +} + +// NewReplicatedEvaluator gets the license data from the kots admin panel +func NewReplicatedEvaluator(domain string) (res *ReplicatedEvaluator) { + return newReplicatedEvaluator(&http.Client{Timeout: replicatedLicenseApiTimeout}, domain) +} diff --git a/components/licensor/typescript/ee/main.go b/components/licensor/typescript/ee/main.go index cf6b374fb21ed2..607283e608b204 100644 --- a/components/licensor/typescript/ee/main.go +++ b/components/licensor/typescript/ee/main.go @@ -7,6 +7,7 @@ package main import ( "C" "encoding/json" + "os" log "github.com/sirupsen/logrus" @@ -14,15 +15,21 @@ import ( ) var ( - instances map[int]*licensor.Evaluator = make(map[int]*licensor.Evaluator) - nextID int = 1 + instances map[int]licensor.Evaluator = make(map[int]licensor.Evaluator) + nextID int = 1 ) // Init initializes the global license evaluator from an environment variable //export Init func Init(key *C.char, domain *C.char) (id int) { id = nextID - instances[id] = licensor.NewEvaluator([]byte(C.GoString(key)), C.GoString(domain)) + switch os.Getenv("GITPOD_LICENSE_TYPE") { + case string(licensor.LicenseTypeReplicated): + instances[id] = licensor.NewReplicatedEvaluator(C.GoString(domain)) + break + default: + instances[id] = licensor.NewGitpodEvaluator([]byte(C.GoString(key)), C.GoString(domain)) + } nextID++ return id diff --git a/install/installer/pkg/cluster/checks.go b/install/installer/pkg/cluster/checks.go index f2ded0dfc907f5..b32603afd121de 100644 --- a/install/installer/pkg/cluster/checks.go +++ b/install/installer/pkg/cluster/checks.go @@ -170,8 +170,9 @@ func checkKubernetesVersion(ctx context.Context, config *rest.Config, namespace } type checkSecretOpts struct { - RequiredFields []string - Validator func(*corev1.Secret) ([]ValidationError, error) + RequiredFields []string + RecommendedFields []string + Validator func(*corev1.Secret) ([]ValidationError, error) } type CheckSecretOpt func(*checkSecretOpts) @@ -182,6 +183,12 @@ func CheckSecretRequiredData(entries ...string) CheckSecretOpt { } } +func CheckSecretRecommendedData(entries ...string) CheckSecretOpt { + return func(cso *checkSecretOpts) { + cso.RecommendedFields = append(cso.RecommendedFields, entries...) + } +} + func CheckSecretRule(validator func(*corev1.Secret) ([]ValidationError, error)) CheckSecretOpt { return func(cso *checkSecretOpts) { cso.Validator = validator @@ -226,6 +233,15 @@ func CheckSecret(name string, opts ...CheckSecretOpt) ValidationCheck { }) } } + for _, k := range cfg.RecommendedFields { + _, ok := secret.Data[k] + if !ok { + res = append(res, ValidationError{ + Message: fmt.Sprintf("secret %s has no %s entry", name, k), + Type: ValidationStatusWarning, + }) + } + } if cfg.Validator != nil { vres, err := cfg.Validator(secret) diff --git a/install/installer/pkg/components/server/deployment.go b/install/installer/pkg/components/server/deployment.go index 98b287ec080938..e8144ee71f2e38 100644 --- a/install/installer/pkg/components/server/deployment.go +++ b/install/installer/pkg/components/server/deployment.go @@ -13,6 +13,7 @@ import ( "github.com/gitpod-io/gitpod/installer/pkg/common" wsmanager "github.com/gitpod-io/gitpod/installer/pkg/components/ws-manager" wsmanagerbridge "github.com/gitpod-io/gitpod/installer/pkg/components/ws-manager-bridge" + configv1 "github.com/gitpod-io/gitpod/installer/pkg/config/v1" appsv1 "k8s.io/api/apps/v1" corev1 "k8s.io/api/core/v1" @@ -53,7 +54,6 @@ func deployment(ctx *common.RenderContext) ([]runtime.Object, error) { Name: "gitpod-license-key", MountPath: licenseFilePath, SubPath: "license", - ReadOnly: true, }) } @@ -164,22 +164,47 @@ func deployment(ctx *common.RenderContext) ([]runtime.Object, error) { common.TracingEnv(ctx), common.AnalyticsEnv(&ctx.Config), common.MessageBusEnv(&ctx.Config), - []corev1.EnvVar{{ - Name: "CONFIG_PATH", - Value: "/config/config.json", - }, { - Name: "IDE_CONFIG_PATH", - Value: "/ide-config/config.json", - }, { - Name: "NODE_ENV", - Value: "production", // todo(sje): will we need to change this? - }, { - Name: "SHLVL", - Value: "1", - }, { - Name: "WSMAN_CFG_MANAGERS", - Value: wsmanCfgManager, - }}, + []corev1.EnvVar{ + { + Name: "CONFIG_PATH", + Value: "/config/config.json", + }, + func() corev1.EnvVar { + envvar := corev1.EnvVar{ + Name: "GITPOD_LICENSE_TYPE", + } + + if ctx.Config.License == nil { + envvar.Value = string(configv1.LicensorTypeGitpod) + } else { + envvar.ValueFrom = &corev1.EnvVarSource{ + SecretKeyRef: &corev1.SecretKeySelector{ + LocalObjectReference: corev1.LocalObjectReference{Name: ctx.Config.License.Name}, + Key: "type", + Optional: pointer.Bool(true), + }, + } + } + + return envvar + }(), + { + Name: "IDE_CONFIG_PATH", + Value: "/ide-config/config.json", + }, + { + Name: "NODE_ENV", + Value: "production", // todo(sje): will we need to change this? + }, + { + Name: "SHLVL", + Value: "1", + }, + { + Name: "WSMAN_CFG_MANAGERS", + Value: wsmanCfgManager, + }, + }, ), // todo(sje): conditionally add github-app-cert-secret in // todo(sje): do we need to cater for serverContainer.volumeMounts from values.yaml? diff --git a/install/installer/pkg/config/v1/config.go b/install/installer/pkg/config/v1/config.go index 9b784bc7c14ddc..69d9e9f30cf3df 100644 --- a/install/installer/pkg/config/v1/config.go +++ b/install/installer/pkg/config/v1/config.go @@ -236,6 +236,13 @@ type OpenVSX struct { URL string `json:"url" validate:"url"` } +type LicensorType string + +const ( + LicensorTypeGitpod LicensorType = "gitpod" + LicensorTypeReplicated LicensorType = "replicated" +) + type FSShiftMethod string const ( diff --git a/install/installer/pkg/config/v1/validation.go b/install/installer/pkg/config/v1/validation.go index a82e77f9bad065..40179f7c461b97 100644 --- a/install/installer/pkg/config/v1/validation.go +++ b/install/installer/pkg/config/v1/validation.go @@ -41,6 +41,11 @@ var FSShiftMethodList = map[FSShiftMethod]struct{}{ FSShiftShiftFS: {}, } +var LicensorTypeList = map[LicensorType]struct{}{ + LicensorTypeGitpod: {}, + LicensorTypeReplicated: {}, +} + // LoadValidationFuncs load custom validation functions for this version of the config API func (v version) LoadValidationFuncs(validate *validator.Validate) error { funcs := map[string]validator.Func{ @@ -120,7 +125,25 @@ func (v version) ClusterValidation(rcfg interface{}) cluster.ValidationChecks { if cfg.License != nil { secretName := cfg.License.Name - res = append(res, cluster.CheckSecret(secretName, cluster.CheckSecretRequiredData("license"))) + licensorKey := "type" + res = append(res, cluster.CheckSecret(secretName, cluster.CheckSecretRequiredData("license"), cluster.CheckSecretRecommendedData(licensorKey), cluster.CheckSecretRule(func(s *corev1.Secret) ([]cluster.ValidationError, error) { + errors := make([]cluster.ValidationError, 0) + + licensor := LicensorType(s.Data[licensorKey]) + if licensor != "" { + // This field is optional, so blank is valid + _, ok := LicensorTypeList[licensor] + + if !ok { + errors = append(errors, cluster.ValidationError{ + Message: fmt.Sprintf("Secret '%s' has invalid license type '%s'", secretName, licensor), + Type: cluster.ValidationStatusError, + }) + } + } + + return errors, nil + }))) } if len(cfg.AuthProviders) > 0 {