Skip to content

Commit

Permalink
Refactor config handling a bit and add flags to init
Browse files Browse the repository at this point in the history
  • Loading branch information
luxas committed Oct 28, 2020
1 parent e165cd0 commit 101e386
Show file tree
Hide file tree
Showing 7 changed files with 114 additions and 62 deletions.
2 changes: 1 addition & 1 deletion cmd/workshopctl/cmd/cleanup.go
Original file line number Diff line number Diff line change
Expand Up @@ -50,7 +50,7 @@ func RunCleanup(cf *CleanupFlags) error {
return err
}

dnsP, err := providers.DNSProviders().NewDNSProvider(ctx, cfg.DNSProvider, cfg.RootDomain)
dnsP, err := providers.DNSProviders().NewDNSProvider(ctx, &cfg.DNSProvider, cfg.RootDomain)
if err != nil {
return err
}
Expand Down
5 changes: 1 addition & 4 deletions cmd/workshopctl/cmd/gen.go
Original file line number Diff line number Diff line change
Expand Up @@ -52,9 +52,6 @@ func loadConfig(ctx context.Context, configPath string) (*config.Config, error)
if err := cfg.Complete(ctx); err != nil {
return nil, err
}
if err := cfg.Validate(); err != nil {
return nil, err
}
return cfg, nil
}

Expand Down Expand Up @@ -91,7 +88,7 @@ func RunGen(gf *GenFlags) error {

// dry-run can be always true here as we're not gonna use the provider for requests, only manifest gen
dnsCtx := util.WithDryRun(ctx, true)
dnsProvider, err := providers.DNSProviders().NewDNSProvider(dnsCtx, cfg.DNSProvider, cfg.RootDomain)
dnsProvider, err := providers.DNSProviders().NewDNSProvider(dnsCtx, &cfg.DNSProvider, cfg.RootDomain)
if err != nil {
return err
}
Expand Down
40 changes: 34 additions & 6 deletions cmd/workshopctl/cmd/init.go
Original file line number Diff line number Diff line change
Expand Up @@ -10,12 +10,16 @@ import (

type InitFlags struct {
*RootFlags
*config.Config

Yes bool
}

// NewInitCommand returns the "init" command
func NewInitCommand(rf *RootFlags) *cobra.Command {
inf := &InitFlags{
RootFlags: rf,
Config: &config.Config{},
}
cmd := &cobra.Command{
Use: "init",
Expand All @@ -31,16 +35,40 @@ func NewInitCommand(rf *RootFlags) *cobra.Command {
return cmd
}

func addInitFlags(fs *pflag.FlagSet, inf *InitFlags) {}
func addInitFlags(fs *pflag.FlagSet, inf *InitFlags) {
fs.StringVar(&inf.Name, "name", inf.Name, "What name this workshop should have")
fs.StringVar(&inf.CloudProvider.ServiceAccountPath, "cloud-provider-service-account-path", inf.CloudProvider.ServiceAccountPath, "Path to service account for cloud provider")
fs.StringVar(&inf.DNSProvider.ServiceAccountPath, "dns-provider-service-account-path", inf.DNSProvider.ServiceAccountPath, "Path to service account for dns provider")
fs.StringVar(&inf.RootDomain, "root-domain", inf.RootDomain, "What the root domain to be managed is")
fs.StringVar(&inf.LetsEncryptEmail, "lets-encrypt-email", inf.LetsEncryptEmail, "What Let's Encrypt email to use")
fs.StringVar(&inf.Git.Repo, "git-repo", inf.Git.Repo, "What git repo to use. By default, try to auto-detect git remote origin.")
fs.StringVar(&inf.Git.ServiceAccountPath, "git-provider-service-account-path", inf.Git.ServiceAccountPath, "Path to service account for git provider")

fs.BoolVarP(&inf.Yes, "yes", "y", inf.Yes, "Overwrite the workshopctl.yaml file although it exists")
}

func RunInit(inf *InitFlags) error {
ctx := util.NewContext(inf.DryRun, inf.RootDir)
if util.FileExists(inf.ConfigPath) {
// TODO: Make this a command-line-input based workflow?
// Don't dry-run, no need for that
ctx := util.NewContext(false, inf.RootDir)
if util.FileExists(inf.ConfigPath) && !inf.Yes {
log.Infof("%s already exists, and --yes isn't specified, won't overwrite file", inf.ConfigPath)
return nil
}
cfg := &config.Config{}
if err := cfg.Complete(ctx); err != nil {

// Try to dynamically figure out the git origin
if inf.Git.Repo == "" {
rootPath := util.JoinPaths(ctx)
origin, _, err := util.ShellCommand(ctx, `git -C %s remote -v | grep push | grep origin | awk '{print $2}'`, rootPath).Run()
if err != nil {
return err
}
inf.Git.Repo = origin
}

if err := inf.Config.Complete(ctx); err != nil {
return err
}
return util.WriteYAMLFile(ctx, inf.ConfigPath, cfg)

return util.WriteYAMLFile(ctx, inf.ConfigPath, inf.Config)
}
2 changes: 1 addition & 1 deletion pkg/apply/apply.go
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@ func Apply(ctx context.Context, cfg *config.Config) error {
return err
}

dnsP, err := providers.DNSProviders().NewDNSProvider(ctx, cfg.DNSProvider, cfg.RootDomain)
dnsP, err := providers.DNSProviders().NewDNSProvider(ctx, &cfg.DNSProvider, cfg.RootDomain)
if err != nil {
return err
}
Expand Down
4 changes: 2 additions & 2 deletions pkg/config/keyval/keyval.go
Original file line number Diff line number Diff line change
Expand Up @@ -26,12 +26,12 @@ func FromClusterInfo(cfg *config.ClusterInfo) *Parameters {
return &Parameters{
WorkshopctlParameters: WorkshopctlParameters{
CloudProvider: cfg.CloudProvider.Name,
CloudProviderServiceAccount: cfg.CloudProvider.InternalToken,
CloudProviderServiceAccount: cfg.CloudProvider.ServiceAccountContent,
CloudProviderSpecific: cfg.CloudProvider.ProviderSpecific,

ExternalDNSProvider: externalDNSMap[cfg.DNSProvider.Name],
TraefikDNSProvider: traefikDNSMap[cfg.DNSProvider.Name],
DNSProviderServiceAccount: cfg.DNSProvider.InternalToken,
DNSProviderServiceAccount: cfg.DNSProvider.ServiceAccountContent,
DNSProviderSpecific: cfg.DNSProvider.ProviderSpecific,

RootDomain: cfg.RootDomain,
Expand Down
99 changes: 64 additions & 35 deletions pkg/config/types.go
Original file line number Diff line number Diff line change
Expand Up @@ -26,17 +26,17 @@ type Config struct {
// CloudProvider specifies what cloud provider to use and how to authenticate with it.
CloudProvider Provider `json:"cloudProvider"`
// DNSProvider specifies what dns provider to use and how to authenticate with it.
// If nil, CloudProvider is used.
DNSProvider *Provider `json:"dnsProvider"`
DNSProvider Provider `json:"dnsProvider"`

RootDomain string `json:"rootDomain"`
Clusters uint16 `json:"clusters"`
// TODO: Add git provider tokens
GitRepo string `json:"gitRepo"`
GitRepoStruct gitprovider.UserRepositoryRef `json:"-"`
// How many clusters should be created?
Clusters uint16 `json:"clusters"`
// Where to store the manifests for collaboration?
Git Git `json:"git"`

// If this is specified you can use "sealed secrets"
GPGKeyID string `json:"gpgKeyID"`
// TODO: Implement this with the help of Mozilla SOPS
// GPGKeyID string `json:"gpgKeyID"`

// Whom to contact by Let's Encrypt
LetsEncryptEmail string `json:"letsEncryptEmail"`
Expand All @@ -49,6 +49,9 @@ type Config struct {
}

func (c *Config) Validate() error {
if c.Name == "" {
return fmt.Errorf("name must not be empty")
}
if c.CloudProvider.ServiceAccountPath == "" {
return fmt.Errorf("must specify cloud provider SA path")
}
Expand All @@ -61,22 +64,29 @@ func (c *Config) Validate() error {
if c.LetsEncryptEmail == "" {
return fmt.Errorf("Let's Encrypt email must not be empty")
}
if c.Name == "" {
return fmt.Errorf("name must not be empty")
if c.Git.Repo == "" {
return fmt.Errorf("must specify backing git repo")
}
if c.Git.ServiceAccountPath == "" {
return fmt.Errorf("must specify git provider token")
}
return nil
}

func (c *Config) Complete(ctx context.Context) error {
// First validate the struct
if err := c.Validate(); err != nil {
return err
}
if c.CloudProvider.Name == "" {
c.CloudProvider.Name = "digitalocean"
}
if c.DNSProvider.Name == "" {
c.DNSProvider.Name = "digitalocean"
}
if c.Clusters == 0 {
c.Clusters = 1
}
if c.DNSProvider == nil {
c.DNSProvider = &c.CloudProvider
}
if c.ClusterLogin.Username == "" {
c.ClusterLogin.Username = "workshopctl"
}
Expand All @@ -90,13 +100,19 @@ func (c *Config) Complete(ctx context.Context) error {
}
if c.CloudProvider.ServiceAccountPath != "" {
saPath := util.JoinPaths(ctx, c.CloudProvider.ServiceAccountPath)
if err := readFileInto(saPath, &c.CloudProvider.InternalToken); err != nil {
if err := readFileInto(saPath, &c.CloudProvider.ServiceAccountContent); err != nil {
return err
}
}
if c.DNSProvider.ServiceAccountPath != "" {
saPath := util.JoinPaths(ctx, c.DNSProvider.ServiceAccountPath)
if err := readFileInto(saPath, &c.DNSProvider.InternalToken); err != nil {
if err := readFileInto(saPath, &c.DNSProvider.ServiceAccountContent); err != nil {
return err
}
}
if c.Git.ServiceAccountPath != "" {
saPath := util.JoinPaths(ctx, c.Git.ServiceAccountPath)
if err := readFileInto(saPath, &c.Git.ServiceAccountContent); err != nil {
return err
}
}
Expand All @@ -112,21 +128,14 @@ func (c *Config) Complete(ctx context.Context) error {
},
}
}
if c.GitRepo == "" {
// TODO: Find a better home for this
rootPath := util.JoinPaths(ctx)
origin, _, err := util.ShellCommand(ctx, `git -C %s remote -v | grep push | grep origin | awk '{print $2}'`, rootPath).Run()
if err != nil {
return err
}
c.GitRepo = origin
}
u, err := giturls.Parse(c.GitRepo)
// Parse the git URL
// TODO: This should live in go-git-providers
u, err := giturls.Parse(c.Git.Repo)
if err != nil {
return err
}
paths := strings.Split(u.Path, "/")
c.GitRepoStruct = gitprovider.UserRepositoryRef{
c.Git.RepoStruct = gitprovider.UserRepositoryRef{
UserRef: gitprovider.UserRef{
Domain: u.Host,
UserLogin: paths[0],
Expand All @@ -136,19 +145,26 @@ func (c *Config) Complete(ctx context.Context) error {
return nil
}

type Provider struct {
// Name of the provider. For now, only "digitalocean" is supported.
Name string `json:"name"`
type ServiceAccount struct {
// ServiceAccountPath specifies the file path to the service account
ServiceAccountPath string `json:"serviceAccountPath"`
// The contents of ServiceAccountPath, read at runtime and never marshalled.
ServiceAccountContent string `json:"-"`
}

ProviderSpecific map[string]string `json:"providerSpecific,omitempty"`

InternalToken string `json:"-"`
// If the ServiceAccount is an oauth2 token, this helper method might be useful for
// the implementing provider
func (sa ServiceAccount) TokenSource() oauth2.TokenSource {
return oauth2.StaticTokenSource(&oauth2.Token{AccessToken: sa.ServiceAccountContent})
}

func (p *Provider) TokenSource() oauth2.TokenSource {
return oauth2.StaticTokenSource(&oauth2.Token{AccessToken: p.InternalToken})
type Provider struct {
// Name of the provider. For now, only "digitalocean" is supported.
Name string `json:"name"`
// The ServiceAccount struct is embedded and inlined into the provider
ServiceAccount `json:",inline"`
// Provider-specific data
ProviderSpecific map[string]string `json:"providerSpecific,omitempty"`
}

type NodeGroup struct {
Expand All @@ -163,6 +179,15 @@ type NodeClaim struct {
Dedicated bool `json:"dedicated"`
}

type Git struct {
// Repo specifies where the "infra" git repo should be
Repo string `json:"repo"`
RepoStruct gitprovider.UserRepositoryRef `json:"-"`

// The ServiceAccount struct is embedded and inlined into this struct
ServiceAccount `json:",inline"`
}

type ClusterLogin struct {
// Username for basic auth logins. Defaults to workshopctl.
Username string `json:"username"`
Expand All @@ -186,14 +211,18 @@ type ClusterInfo struct {
Password string
}

func NewClusterInfo(cfg *Config, i ClusterNumber) *ClusterInfo {
func NewClusterInfo(ctx context.Context, cfg *Config, i ClusterNumber) *ClusterInfo {
pass := cfg.ClusterLogin.CommonPassword
if cfg.ClusterLogin.UniquePasswords {
var err error
pass, err = util.RandomSHA(4) // TODO: constant
if err != nil {
panic(err)
}
// Warn about possible misconfigurations
if len(cfg.ClusterLogin.CommonPassword) != 0 {
util.Logger(ctx).Warnf("You have specified both .ClusterLogin.UniquePasswords and .ClusterLogin.CommonPassword. UniquePasswords has higher priority and hence CommonPassword is ignored.")
}
}
return &ClusterInfo{cfg, i, pass}
}
Expand Down Expand Up @@ -250,7 +279,7 @@ func ForCluster(ctx context.Context, n uint16, cfg *Config, fn func(context.Cont
clusterCtx = util.WithMutex(clusterCtx, mux)
logger := util.Logger(clusterCtx)
logger.Tracef("ForCluster goroutine starting...")
clusterInfo := NewClusterInfo(cfg, j)
clusterInfo := NewClusterInfo(clusterCtx, cfg, j)
if err := fn(clusterCtx, clusterInfo); err != nil {
logger.Error(err)
foundErr = true
Expand Down
24 changes: 11 additions & 13 deletions pkg/gotk/gotk.go
Original file line number Diff line number Diff line change
Expand Up @@ -37,20 +37,13 @@ func SetupGitOps(ctx context.Context, info *config.ClusterInfo) error {
}

var provider string
switch info.GitRepoStruct.Domain {
switch info.Git.RepoStruct.Domain {
case "github.com":
provider = "github"
case "gitlab.com":
provider = "gitlab"
default:
return fmt.Errorf("git repo %s: unknown provider domain", info.GitRepo)
}

// Forward the env var
envVarName := fmt.Sprintf("%s_TOKEN", strings.ToUpper(provider))
envVarVal := os.Getenv(envVarName)
if envVarVal == "" {
return fmt.Errorf("Need to set env var: %s", envVarName)
return fmt.Errorf("git repo %s: unknown provider domain", info.Git.Repo)
}

// TODO: Upstream gotk doesn't support the --kubeconfig flag in install/bootstrap at least
Expand All @@ -66,8 +59,8 @@ func SetupGitOps(ctx context.Context, info *config.ClusterInfo) error {
kubeConfigArg,
"bootstrap",
provider,
"--owner="+info.GitRepoStruct.UserLogin,
"--repository="+info.GitRepoStruct.RepositoryName,
"--owner="+info.Git.RepoStruct.UserLogin,
"--repository="+info.Git.RepoStruct.RepositoryName,
"--path="+info.Index.ClusterDir(),
// Only install these two for now. TODO: In the future, also include notifications
"--components=source-controller,kustomize-controller,helm-controller",
Expand All @@ -76,7 +69,12 @@ func SetupGitOps(ctx context.Context, info *config.ClusterInfo) error {
// TODO: Assuming personal for now
"--personal",
).WithStdio(nil, os.Stdout, os.Stderr).
WithEnv(fmt.Sprintf("%s=%s", envVarName, envVarVal), kubeConfigEnv, "PATH="+os.Getenv("PATH")).
Run()
WithEnv(
// Forward the {GITHUB,GITLAB}_TOKEN variable from the config file
fmt.Sprintf("%s_TOKEN=%s", strings.ToUpper(provider), info.Git.ServiceAccountContent),
// Forward the PATH variable
fmt.Sprintf("PATH=%s", os.Getenv("PATH")),
kubeConfigEnv,
).Run()
return err
}

0 comments on commit 101e386

Please sign in to comment.