diff --git a/cmd/login/login.go b/cmd/login/login.go index 319f813e..1387567a 100644 --- a/cmd/login/login.go +++ b/cmd/login/login.go @@ -20,10 +20,12 @@ import ( "github.com/rs/zerolog" "github.com/spf13/cobra" + "github.com/smartcontractkit/cre-cli/internal/client/graphqlclient" "github.com/smartcontractkit/cre-cli/internal/constants" "github.com/smartcontractkit/cre-cli/internal/credentials" "github.com/smartcontractkit/cre-cli/internal/environments" "github.com/smartcontractkit/cre-cli/internal/runtime" + "github.com/smartcontractkit/cre-cli/internal/tenantctx" "github.com/smartcontractkit/cre-cli/internal/ui" ) @@ -114,6 +116,11 @@ func (h *handler) execute() error { return err } + h.spinner.Update("Fetching tenant configuration...") + if err := h.fetchTenantConfig(tokenSet); err != nil { + h.log.Warn().Err(err).Msg("failed to fetch tenant config — context.yaml not written") + } + // Stop spinner before final output h.spinner.Stop() @@ -383,6 +390,21 @@ func (h *handler) exchangeCodeForTokens(ctx context.Context, code string) (*cred return &tokenSet, nil } +func (h *handler) fetchTenantConfig(tokenSet *credentials.CreLoginTokenSet) error { + creds := &credentials.Credentials{ + Tokens: tokenSet, + AuthType: credentials.AuthTypeBearer, + } + gqlClient := graphqlclient.New(creds, h.environmentSet, h.log) + + envName := h.environmentSet.EnvName + if envName == "" { + envName = environments.DefaultEnv + } + + return tenantctx.FetchAndWriteContext(context.Background(), gqlClient, envName, h.log) +} + func openBrowser(urlStr string, goos string) error { switch goos { case "darwin": diff --git a/internal/tenantctx/tenantctx.go b/internal/tenantctx/tenantctx.go new file mode 100644 index 00000000..23ea4cf4 --- /dev/null +++ b/internal/tenantctx/tenantctx.go @@ -0,0 +1,156 @@ +package tenantctx + +import ( + "context" + "fmt" + "os" + "path/filepath" + "strings" + + "github.com/machinebox/graphql" + "github.com/rs/zerolog" + "gopkg.in/yaml.v2" + + "github.com/smartcontractkit/cre-cli/internal/client/graphqlclient" + "github.com/smartcontractkit/cre-cli/internal/credentials" +) + +const ( + ContextFile = "context.yaml" +) + +// Registry represents a single workflow registry from the GQL response. +type Registry struct { + ID string `yaml:"id" json:"id"` + Label string `yaml:"label" json:"label"` + Type string `yaml:"type" json:"type"` + ChainSelector *string `yaml:"chain_selector,omitempty" json:"chainSelector,omitempty"` + Address *string `yaml:"address,omitempty" json:"address,omitempty"` + SecretsAuthFlows []string `yaml:"secrets_auth_flows" json:"secretsAuthFlows"` + Active bool `yaml:"active" json:"-"` +} + +// EnvironmentContext represents the tenant config for a single environment block in context.yaml. +type EnvironmentContext struct { + TenantID string `yaml:"tenant_id"` + DefaultDonFamily string `yaml:"default_don_family"` + VaultGatewayURL string `yaml:"vault_gateway_url"` + Registries []*Registry `yaml:"registries"` +} + +// getTenantConfigResponse mirrors the GQL response shape. +type getTenantConfigResponse struct { + GetTenantConfig struct { + TenantID int `json:"tenantId"` + DefaultDonFamily string `json:"defaultDonFamily"` + VaultGatewayURL string `json:"vaultGatewayUrl"` + Registries []struct { + ID string `json:"id"` + Label string `json:"label"` + Type string `json:"type"` + ChainSelector *string `json:"chainSelector"` + Address *string `json:"address"` + SecretsAuthFlows []string `json:"secretsAuthFlows"` + } `json:"registries"` + } `json:"getTenantConfig"` +} + +const getTenantConfigQuery = `query GetTenantConfig { + getTenantConfig { + tenantId + defaultDonFamily + vaultGatewayUrl + registries { + id + label + type + chainSelector + address + secretsAuthFlows + } + } +}` + +// FetchAndWriteContext calls getTenantConfig and writes ~/.cre/context.yaml. +// envName is the CRE_CLI_ENV value (e.g. "PRODUCTION", "STAGING"). +func FetchAndWriteContext(ctx context.Context, gqlClient *graphqlclient.Client, envName string, log *zerolog.Logger) error { + req := graphql.NewRequest(getTenantConfigQuery) + + var resp getTenantConfigResponse + if err := gqlClient.Execute(ctx, req, &resp); err != nil { + return fmt.Errorf("fetch tenant config: %w", err) + } + + tc := resp.GetTenantConfig + + registries := make([]*Registry, 0, len(tc.Registries)) + for _, r := range tc.Registries { + registries = append(registries, &Registry{ + ID: r.ID, + Label: r.Label, + Type: mapRegistryType(r.Type), + ChainSelector: r.ChainSelector, + Address: r.Address, + SecretsAuthFlows: r.SecretsAuthFlows, + Active: false, + }) + } + + // Default the first registry to active + if len(registries) > 0 { + registries[0].Active = true + } + + envCtx := &EnvironmentContext{ + TenantID: fmt.Sprintf("%d", tc.TenantID), + DefaultDonFamily: tc.DefaultDonFamily, + VaultGatewayURL: tc.VaultGatewayURL, + Registries: registries, + } + + contextMap := map[string]*EnvironmentContext{ + strings.ToUpper(envName): envCtx, + } + + return writeContextFile(contextMap, log) +} + +func mapRegistryType(gqlType string) string { + switch gqlType { + case "ON_CHAIN": + return "on-chain" + case "OFF_CHAIN": + return "off-chain" + default: + return strings.ToLower(gqlType) + } +} + +func writeContextFile(data map[string]*EnvironmentContext, log *zerolog.Logger) error { + home, err := os.UserHomeDir() + if err != nil { + return fmt.Errorf("get home dir: %w", err) + } + + dir := filepath.Join(home, credentials.ConfigDir) + if err := os.MkdirAll(dir, 0o700); err != nil { + return fmt.Errorf("create config dir: %w", err) + } + + out, err := yaml.Marshal(data) + if err != nil { + return fmt.Errorf("marshal context: %w", err) + } + + path := filepath.Join(dir, ContextFile) + tmp := path + ".tmp" + if err := os.WriteFile(tmp, out, 0o600); err != nil { + return fmt.Errorf("write temp file: %w", err) + } + if err := os.Rename(tmp, path); err != nil { + return fmt.Errorf("rename temp file: %w", err) + } + + log.Debug().Str("path", path).Msg("wrote context.yaml") + return nil +}