diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 1379e10..c839661 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -56,7 +56,7 @@ jobs: shell: bash run: echo 'flags=--skip homebrew' >> $GITHUB_ENV - name: Run GoReleaser - uses: goreleaser/goreleaser-action@v5 + uses: goreleaser/goreleaser-action@v6 with: distribution: goreleaser-pro version: latest @@ -97,7 +97,7 @@ jobs: enableCrossOsArchive: true - name: Run GoReleaser - uses: goreleaser/goreleaser-action@v5 + uses: goreleaser/goreleaser-action@v6 with: distribution: goreleaser-pro version: latest diff --git a/api/acme.go b/api/acme.go new file mode 100644 index 0000000..5c19f73 --- /dev/null +++ b/api/acme.go @@ -0,0 +1,40 @@ +package api + +import ( + "crypto/tls" + "encoding/base64" + "time" + + "github.com/anchordotdev/cli" + "golang.org/x/crypto/acme" + "golang.org/x/crypto/acme/autocert" +) + +func ProvisionCert(eab *Eab, domains []string, acmeURL string) (*tls.Certificate, error) { + hmacKey, err := base64.URLEncoding.DecodeString(eab.HmacKey) + if err != nil { + return nil, err + } + + mgr := &autocert.Manager{ + Prompt: autocert.AcceptTOS, + HostPolicy: autocert.HostWhitelist(domains...), + Client: &acme.Client{ + DirectoryURL: acmeURL, + UserAgent: cli.UserAgent(), + }, + ExternalAccountBinding: &acme.ExternalAccountBinding{ + KID: eab.Kid, + Key: hmacKey, + }, + RenewBefore: 24 * time.Hour, + } + + // TODO: switch to using ACME package here, so that extra domains can be sent through for SAN extension + clientHello := &tls.ClientHelloInfo{ + ServerName: domains[0], + CipherSuites: []uint16{tls.TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256}, + } + + return mgr.GetCertificate(clientHello) +} diff --git a/api/api.go b/api/api.go index 301cb74..ad344f9 100644 --- a/api/api.go +++ b/api/api.go @@ -26,6 +26,18 @@ var ( ErrGnomeKeyringRequired = fmt.Errorf("gnome-keyring required for secure credential storage: %w", ErrSignedOut) ) +type QueryParam func(url.Values) + +type QueryParams []QueryParam + +func (q QueryParams) Apply(u *url.URL) { + val := u.Query() + for _, fn := range q { + fn(val) + } + u.RawQuery = val.Encode() +} + // NB: can't call this Client since the name is already taken by an openapi // generated type. It's more like a session anyways, since it caches some // current user info. @@ -222,6 +234,34 @@ func (s *Session) FetchCredentials(ctx context.Context, orgSlug, realmSlug strin return creds.Items, nil } +func getCredentialsURL(orgSlug, realmSlug string) (*url.URL, error) { + return url.Parse(fetchCredentialsPath(orgSlug, realmSlug)) +} + +func SubCA(apid string) QueryParam { + return func(v url.Values) { + // TODO: v.Set("type", "subca") + v.Set("subject_uid_param", apid) + } +} + +func (s *Session) GetCredentials(ctx context.Context, orgSlug, realmSlug string, params ...QueryParam) ([]Credential, error) { + var creds struct { + Items []Credential `json:"items,omitempty"` + } + + u, err := getCredentialsURL(orgSlug, realmSlug) + if err != nil { + return nil, err + } + QueryParams(params).Apply(u) + + if err := s.get(ctx, u.RequestURI(), &creds); err != nil { + return nil, err + } + return creds.Items, nil +} + func (s *Session) UserInfo(ctx context.Context) (*Root, error) { if s.userInfo != nil { return s.userInfo, nil @@ -293,11 +333,14 @@ func (s *Session) GetService(ctx context.Context, orgSlug, serviceSlug string) ( return &svc, nil } -func (s *Session) get(ctx context.Context, path string, out any) error { - req, err := http.NewRequestWithContext(ctx, "GET", path, nil) +func (s *Session) get(ctx context.Context, uri string, out any) error { + req, err := http.NewRequestWithContext(ctx, "GET", uri, nil) if err != nil { return err } + if req.URL, err = url.Parse(uri); err != nil { + return err + } req.Header.Set("Content-Type", "application/json") res, err := s.Do(req) @@ -426,6 +469,7 @@ func (r urlRewriter) RoundTripper(next http.RoundTripper) http.RoundTripper { if err != nil { return nil, err } + u.RawQuery = req.URL.RawQuery req.URL = u.JoinPath(req.URL.Path) return next.RoundTrip(req) diff --git a/api/openapi.gen.go b/api/openapi.gen.go index 3d841b3..f15dd20 100644 --- a/api/openapi.gen.go +++ b/api/openapi.gen.go @@ -46,6 +46,9 @@ type Attachment struct { // Domains A list of domains for this attachment. Domains []string `json:"domains"` + // Port TCP port number for the service in the realm. + Port int32 `json:"port"` + // Relationships Values used as parameters when referencing related resources. Relationships struct { Chain RelationshipsChainApid `json:"chain"` @@ -346,6 +349,9 @@ type PathServiceParam = string // QueryCaParam defines model for query_ca_param. type QueryCaParam = string +// QuerySubjectUidParam defines model for query_subject_uid_param. +type QuerySubjectUidParam = string + // ServicesXtach200 defines model for services_xtach_200. type ServicesXtach200 struct { // Domains A list of domains for this attachment. @@ -404,6 +410,9 @@ type CreateClientJSONBody struct { type GetCredentialsParams struct { // CaParam ca for operation CaParam *QueryCaParam `form:"ca_param,omitempty" json:"ca_param,omitempty"` + + // SubjectUidParam subject uid for operation + SubjectUidParam *QuerySubjectUidParam `form:"subject_uid_param,omitempty" json:"subject_uid_param,omitempty"` } // AttachOrgServiceJSONBody defines parameters for AttachOrgService. diff --git a/cli.go b/cli.go index cf1e964..b656beb 100644 --- a/cli.go +++ b/cli.go @@ -12,6 +12,7 @@ import ( "time" "github.com/cli/browser" + "github.com/google/go-github/v54/github" "github.com/mcuadros/go-defaults" "github.com/spf13/cobra" "github.com/spf13/pflag" @@ -41,10 +42,52 @@ var Version = struct { Arch: runtime.GOARCH, } +var LatestRelease *Release + +type Release = github.RepositoryRelease + +var SkipReleaseCheck = false + func IsDevVersion() bool { return Version.Version == "dev" } +func IsFreshLatestRelease(ctx context.Context) (bool, error) { + release, err := getLatestRelease(ctx) + if err != nil { + return true, err + } + + return release.PublishedAt != nil && time.Since(release.PublishedAt.Time).Hours() < 24, nil +} + +func IsUpgradeable(ctx context.Context) (bool, error) { + release, err := getLatestRelease(ctx) + if err != nil { + return false, err + } + + return release.TagName != nil && *release.TagName != ReleaseTagName(), nil +} + +func getLatestRelease(ctx context.Context) (*Release, error) { + if LatestRelease != nil { + return LatestRelease, nil + } + + release, _, err := github.NewClient(nil).Repositories.GetLatestRelease(ctx, "anchordotdev", "cli") + if err != nil { + return nil, err + } + + LatestRelease = &Release{ + PublishedAt: release.PublishedAt, + TagName: release.TagName, + } + + return LatestRelease, nil +} + func UserAgent() string { return "Anchor CLI " + VersionString() } diff --git a/cli_test.go b/cli_test.go index fbb9baa..4cea7a7 100644 --- a/cli_test.go +++ b/cli_test.go @@ -11,7 +11,6 @@ import ( "time" "github.com/anchordotdev/cli" - "github.com/anchordotdev/cli/models" "github.com/anchordotdev/cli/stacktrace" _ "github.com/anchordotdev/cli/testflags" "github.com/anchordotdev/cli/ui" @@ -169,35 +168,3 @@ func (m *TestHint) View() string { } var Timestamp, _ = time.Parse(time.RFC3339Nano, "2024-01-02T15:04:05.987654321Z") - -func TestConfigLoadTOMLGolden(t *testing.T) { - ctx, cancel := context.WithCancel(context.Background()) - defer cancel() - - cfg := new(cli.Config) - cfg.File.Path = "anchor.toml" - cfg.Via.TOML = new(cli.Config) - - ctx = cli.ContextWithConfig(ctx, cfg) - - cmd := tomlCommand{} - - uitest.TestTUIOutput(ctx, t, cmd.UI()) -} - -type tomlCommand struct{} - -func (c tomlCommand) UI() cli.UI { - return cli.UI{ - RunTUI: c.run, - } -} - -func (*tomlCommand) run(ctx context.Context, drv *ui.Driver) error { - cfg := cli.ConfigFromContext(ctx) - if cfg.Via.TOML != nil { - drv.Activate(ctx, models.ConfigLoaded(cfg.File.Path)) - } - - return nil -} diff --git a/cmd.go b/cmd.go index a66c643..aea0212 100644 --- a/cmd.go +++ b/cmd.go @@ -5,7 +5,6 @@ import ( "errors" "github.com/MakeNowJust/heredoc" - "github.com/anchordotdev/cli/models" "github.com/anchordotdev/cli/stacktrace" "github.com/anchordotdev/cli/ui" "github.com/spf13/cobra" @@ -156,6 +155,13 @@ var rootDef = CmdDef{ Args: cobra.NoArgs, Short: "Fetch Environment Variables for Service", }, + { + Name: "probe", + + Use: "probe [flags]", + Args: cobra.NoArgs, + Short: "Probe a service for proper TLS setup & configuration", + }, }, }, { @@ -206,7 +212,16 @@ var rootDef = CmdDef{ Use: "version", Args: cobra.NoArgs, - Short: "Show version info", + Short: "Show Version Info", + SubDefs: []CmdDef{ + { + Name: "upgrade", + + Use: "upgrade", + Args: cobra.NoArgs, + Short: "Check for Upgrade", + }, + }, }, }, } @@ -302,10 +317,6 @@ func NewCmd[T UIer](parent *cobra.Command, name string, fn func(*cobra.Command)) errc <- err }() - if cfg.Via.TOML != nil { - drv.Activate(ctx, models.ConfigLoaded(cfg.File.Path)) - } - if err := stacktrace.CapturePanic(func() error { return t.UI().RunTUI(ctx, drv) }); err != nil { var uierr ui.Error if errors.As(err, &uierr) { diff --git a/config.go b/config.go index a8013b1..7b3fe6e 100644 --- a/config.go +++ b/config.go @@ -5,6 +5,7 @@ import ( "context" "io" "io/fs" + "net" "net/url" "os" "runtime" @@ -71,6 +72,10 @@ type Config struct { EnvOutput string `env:"ENV_OUTPUT" toml:",omitempty,readonly"` CertStyle string `env:"CERT_STYLE" toml:"cert-style,omitempty"` + + Probe struct { + Timeout time.Duration `default:"2m" env:"PROBE_TIMEOUT" toml:",omitempty,readonly"` + } `toml:",omitempty,readonly"` } `toml:"service,omitempty"` Trust struct { @@ -98,6 +103,16 @@ type Config struct { } `toml:",omitempty,readonly"` } +type Dialer interface { + DialContext(context.Context, string, string) (net.Conn, error) +} + +type DialFunc func(context.Context, string, string) (net.Conn, error) + +func (fn DialFunc) DialContext(ctx context.Context, network string, address string) (net.Conn, error) { + return fn(ctx, network, address) +} + // values used for testing type ConfigTest struct { Prefer map[string]ConfigTestPrefer // values for prism prefer header @@ -105,13 +120,15 @@ type ConfigTest struct { ACME struct { URL string } - Browserless bool // run as though browserless - GOOS string // change OS identifier in tests - ProcFS fs.FS // change the proc filesystem in tests - LclHostPort int // specify lcl host port in tests - SkipRunE bool // skip RunE for testing purposes - SystemFS SystemFS // change the system filesystem in tests - Timestamp time.Time // timestamp to use/display in tests + Browserless bool // run as though browserless + GOOS string // change OS identifier in tests + ProcFS fs.FS // change the proc filesystem in tests + LclHostPort int // specify lcl host port in tests + SkipRunE bool // skip RunE for testing purposes + SystemFS SystemFS // change the system filesystem in tests + Timestamp time.Time // timestamp to use/display in tests + NetResolver *net.Resolver // DNS resolver for (some) tests + NetDialer Dialer // TCP dialer for (some) tests } type ConfigTestPrefer struct { @@ -231,8 +248,6 @@ func (c *Config) loadENV() error { } func (c *Config) loadTOML(fsys fs.FS) error { - c.Via.TOML = new(Config) - if path, ok := os.LookupEnv("ANCHOR_CONFIG"); ok { c.File.Path = path } @@ -292,7 +307,7 @@ func (c *Config) ViaSource(fetcher func(*Config) any) string { return "env" } - if fetcher(c.Via.TOML) == value { + if c.Via.TOML != nil && fetcher(c.Via.TOML) == value { return c.File.Path } diff --git a/go.mod b/go.mod index a2c49ff..d51bba8 100644 --- a/go.mod +++ b/go.mod @@ -1,21 +1,22 @@ module github.com/anchordotdev/cli -go 1.23.0 +go 1.23.2 require ( github.com/MakeNowJust/heredoc v1.0.0 github.com/Masterminds/semver v1.5.0 github.com/atotto/clipboard v0.1.4 github.com/aymanbagabas/go-udiff v0.2.0 + github.com/benburkert/dns v0.0.0-20190225204957-d356cf78cdfc github.com/brianvoe/gofakeit/v7 v7.0.4 - github.com/charmbracelet/bubbles v0.19.0 - github.com/charmbracelet/bubbletea v0.27.0 - github.com/charmbracelet/lipgloss v0.12.1 + github.com/charmbracelet/bubbles v0.20.0 + github.com/charmbracelet/bubbletea v1.1.0 + github.com/charmbracelet/lipgloss v0.13.0 github.com/charmbracelet/x/exp/teatest v0.0.0-20240222131549-03ee51df8bea github.com/cli/browser v1.3.0 github.com/deepmap/oapi-codegen v1.16.3 github.com/fatih/structtag v1.2.0 - github.com/go-test/deep v1.0.8 + github.com/go-test/deep v1.1.1 github.com/gofrs/flock v0.12.1 github.com/google/go-github/v54 v54.0.0 github.com/inetaf/tcpproxy v0.0.0-20240214030015-3ce58045626c @@ -24,17 +25,17 @@ require ( github.com/mohae/deepcopy v0.0.0-20170929034955-c48cc78d4826 github.com/muesli/termenv v0.15.2 github.com/oapi-codegen/runtime v1.1.1 - github.com/pelletier/go-toml/v2 v2.2.2 + github.com/pelletier/go-toml/v2 v2.2.3 github.com/r3labs/diff/v3 v3.0.1 github.com/spf13/cobra v1.8.1 github.com/spf13/pflag v1.0.5 github.com/stretchr/testify v1.9.0 github.com/zalando/go-keyring v0.2.5 - golang.org/x/crypto v0.26.0 + golang.org/x/crypto v0.28.0 golang.org/x/exp v0.0.0-20240318143956-a85f2c67cd81 golang.org/x/sync v0.8.0 - golang.org/x/sys v0.24.0 - golang.org/x/text v0.17.0 + golang.org/x/sys v0.26.0 + golang.org/x/text v0.19.0 howett.net/plist v1.0.1 ) @@ -42,11 +43,9 @@ require ( github.com/ProtonMail/go-crypto v0.0.0-20230217124315-7d5c6f04bbb8 // indirect github.com/alessio/shellescape v1.4.1 // indirect github.com/aymanbagabas/go-osc52/v2 v2.0.1 // indirect - github.com/charmbracelet/x/ansi v0.1.4 // indirect + github.com/charmbracelet/x/ansi v0.2.3 // indirect github.com/charmbracelet/x/exp/golden v0.0.0-20240815200342-61de596daa2b // indirect - github.com/charmbracelet/x/input v0.1.0 // indirect - github.com/charmbracelet/x/term v0.1.1 // indirect - github.com/charmbracelet/x/windows v0.1.0 // indirect + github.com/charmbracelet/x/term v0.2.0 // indirect github.com/cloudflare/circl v1.3.7 // indirect github.com/danieljoos/wincred v1.2.0 // indirect github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect @@ -75,9 +74,8 @@ require ( github.com/sahilm/fuzzy v0.1.1 // indirect github.com/vmihailenco/msgpack/v5 v5.3.5 // indirect github.com/vmihailenco/tagparser/v2 v2.0.0 // indirect - github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e // indirect - golang.org/x/mod v0.20.0 // indirect - golang.org/x/net v0.28.0 // indirect + golang.org/x/mod v0.21.0 // indirect + golang.org/x/net v0.30.0 // indirect golang.org/x/oauth2 v0.18.0 // indirect golang.org/x/tools v0.21.1-0.20240508182429-e35e4ccd0d2d // indirect google.golang.org/appengine v1.6.8 // indirect diff --git a/go.sum b/go.sum index 9637c94..4fa3167 100644 --- a/go.sum +++ b/go.sum @@ -13,27 +13,25 @@ github.com/aymanbagabas/go-osc52/v2 v2.0.1 h1:HwpRHbFMcZLEVr42D4p7XBqjyuxQH5SMiE github.com/aymanbagabas/go-osc52/v2 v2.0.1/go.mod h1:uYgXzlJ7ZpABp8OJ+exZzJJhRNQ2ASbcXHWsFqH8hp8= github.com/aymanbagabas/go-udiff v0.2.0 h1:TK0fH4MteXUDspT88n8CKzvK0X9O2xu9yQjWpi6yML8= github.com/aymanbagabas/go-udiff v0.2.0/go.mod h1:RE4Ex0qsGkTAJoQdQQCA0uG+nAzJO/pI/QwceO5fgrA= +github.com/benburkert/dns v0.0.0-20190225204957-d356cf78cdfc h1:eyDlmf21vuKN61WoxV2cQLDH/PBDyyjIhUI4kT2o1yM= +github.com/benburkert/dns v0.0.0-20190225204957-d356cf78cdfc/go.mod h1:6ul4nJKqsreAIBK5lUkibcUn2YBU6CvDzlKDH+dtZsQ= github.com/brianvoe/gofakeit/v7 v7.0.4 h1:Mkxwz9jYg8Ad8NvT9HA27pCMZGFQo08MK6jD0QTKEww= github.com/brianvoe/gofakeit/v7 v7.0.4/go.mod h1:QXuPeBw164PJCzCUZVmgpgHJ3Llj49jSLVkKPMtxtxA= github.com/bwesterb/go-ristretto v1.2.0/go.mod h1:fUIoIZaG73pV5biE2Blr2xEzDoMj7NFEuV9ekS419A0= -github.com/charmbracelet/bubbles v0.19.0 h1:gKZkKXPP6GlDk6EcfujDK19PCQqRjaJZQ7QRERx1UF0= -github.com/charmbracelet/bubbles v0.19.0/go.mod h1:WILteEqZ+krG5c3ntGEMeG99nCupcuIk7V0/zOP0tOA= -github.com/charmbracelet/bubbletea v0.27.0 h1:Mznj+vvYuYagD9Pn2mY7fuelGvP0HAXtZYGgRBCbHvU= -github.com/charmbracelet/bubbletea v0.27.0/go.mod h1:5MdP9XH6MbQkgGhnlxUqCNmBXf9I74KRQ8HIidRxV1Y= -github.com/charmbracelet/lipgloss v0.12.1 h1:/gmzszl+pedQpjCOH+wFkZr/N90Snz40J/NR7A0zQcs= -github.com/charmbracelet/lipgloss v0.12.1/go.mod h1:V2CiwIuhx9S1S1ZlADfOj9HmxeMAORuz5izHb0zGbB8= -github.com/charmbracelet/x/ansi v0.1.4 h1:IEU3D6+dWwPSgZ6HBH+v6oUuZ/nVawMiWj5831KfiLM= -github.com/charmbracelet/x/ansi v0.1.4/go.mod h1:dk73KoMTT5AX5BsX0KrqhsTqAnhZZoCBjs7dGWp4Ktw= +github.com/charmbracelet/bubbles v0.20.0 h1:jSZu6qD8cRQ6k9OMfR1WlM+ruM8fkPWkHvQWD9LIutE= +github.com/charmbracelet/bubbles v0.20.0/go.mod h1:39slydyswPy+uVOHZ5x/GjwVAFkCsV8IIVy+4MhzwwU= +github.com/charmbracelet/bubbletea v1.1.0 h1:FjAl9eAL3HBCHenhz/ZPjkKdScmaS5SK69JAK2YJK9c= +github.com/charmbracelet/bubbletea v1.1.0/go.mod h1:9Ogk0HrdbHolIKHdjfFpyXJmiCzGwy+FesYkZr7hYU4= +github.com/charmbracelet/lipgloss v0.13.0 h1:4X3PPeoWEDCMvzDvGmTajSyYPcZM4+y8sCA/SsA3cjw= +github.com/charmbracelet/lipgloss v0.13.0/go.mod h1:nw4zy0SBX/F/eAO1cWdcvy6qnkDUxr8Lw7dvFrAIbbY= +github.com/charmbracelet/x/ansi v0.2.3 h1:VfFN0NUpcjBRd4DnKfRaIRo53KRgey/nhOoEqosGDEY= +github.com/charmbracelet/x/ansi v0.2.3/go.mod h1:dk73KoMTT5AX5BsX0KrqhsTqAnhZZoCBjs7dGWp4Ktw= github.com/charmbracelet/x/exp/golden v0.0.0-20240815200342-61de596daa2b h1:MnAMdlwSltxJyULnrYbkZpp4k58Co7Tah3ciKhSNo0Q= github.com/charmbracelet/x/exp/golden v0.0.0-20240815200342-61de596daa2b/go.mod h1:wDlXFlCrmJ8J+swcL/MnGUuYnqgQdW9rhSD61oNMb6U= github.com/charmbracelet/x/exp/teatest v0.0.0-20240222131549-03ee51df8bea h1:rMsCa4AcGApEidjhRpitA2HZds22ZSnAuVjx8SVF3yA= github.com/charmbracelet/x/exp/teatest v0.0.0-20240222131549-03ee51df8bea/go.mod h1:SG24wGkG/mix5V2dZLXfQ6Bod43HGvk9CkTDxATwKN4= -github.com/charmbracelet/x/input v0.1.0 h1:TEsGSfZYQyOtp+STIjyBq6tpRaorH0qpwZUj8DavAhQ= -github.com/charmbracelet/x/input v0.1.0/go.mod h1:ZZwaBxPF7IG8gWWzPUVqHEtWhc1+HXJPNuerJGRGZ28= -github.com/charmbracelet/x/term v0.1.1 h1:3cosVAiPOig+EV4X9U+3LDgtwwAoEzJjNdwbXDjF6yI= -github.com/charmbracelet/x/term v0.1.1/go.mod h1:wB1fHt5ECsu3mXYusyzcngVWWlu1KKUmmLhfgr/Flxw= -github.com/charmbracelet/x/windows v0.1.0 h1:gTaxdvzDM5oMa/I2ZNF7wN78X/atWemG9Wph7Ika2k4= -github.com/charmbracelet/x/windows v0.1.0/go.mod h1:GLEO/l+lizvFDBPLIOk+49gdX49L9YWMB5t+DZd0jkQ= +github.com/charmbracelet/x/term v0.2.0 h1:cNB9Ot9q8I711MyZ7myUR5HFWL/lc3OpU8jZ4hwm0x0= +github.com/charmbracelet/x/term v0.2.0/go.mod h1:GVxgxAbjUrmpvIINHIQnJJKpMlHiZ4cktEQCN6GWyF0= github.com/cli/browser v1.3.0 h1:LejqCrpWr+1pRqmEPDGnTZOjsMe7sehifLynZJuqJpo= github.com/cli/browser v1.3.0/go.mod h1:HH8s+fOAxjhQoBUAsKuPCbqUuxZDhQ2/aD+SzsEfBTk= github.com/cloudflare/circl v1.1.0/go.mod h1:prBCrKB9DV4poKZY1l9zBXg2QJY7mvgRvtMxxK7fi4I= @@ -60,8 +58,9 @@ github.com/go-openapi/jsonpointer v0.20.0/go.mod h1:6PGzBjjIIumbLYysB73Klnms1mwn github.com/go-openapi/swag v0.19.5/go.mod h1:POnQmlKehdgb5mhVOsnJFsivZCEZ/vjK9gh66Z9tfKk= github.com/go-openapi/swag v0.22.4 h1:QLMzNJnMGPRNDCbySlcj1x01tzU8/9LTTL9hZZZogBU= github.com/go-openapi/swag v0.22.4/go.mod h1:UzaqsxGiab7freDnrUUra0MwWfN/q7tE4j+VcZ0yl14= -github.com/go-test/deep v1.0.8 h1:TDsG77qcSprGbC6vTN8OuXp5g+J+b5Pcguhf7Zt61VM= github.com/go-test/deep v1.0.8/go.mod h1:5C2ZWiW0ErCdrYzpqxLbTX7MG14M9iiw8DgHncVwcsE= +github.com/go-test/deep v1.1.1 h1:0r/53hagsehfO4bzD2Pgr/+RgHqhmf+k1Bpse2cTu1U= +github.com/go-test/deep v1.1.1/go.mod h1:5C2ZWiW0ErCdrYzpqxLbTX7MG14M9iiw8DgHncVwcsE= github.com/godbus/dbus/v5 v5.1.0 h1:4KLkAxT3aOY8Li4FRJe/KvhoNFFxo0m6fNuFUO8QJUk= github.com/godbus/dbus/v5 v5.1.0/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA= github.com/gofrs/flock v0.12.1 h1:MTLVXXHf8ekldpJk3AKicLij9MdwOWkZ+a/jHHZby9E= @@ -126,8 +125,8 @@ github.com/muesli/termenv v0.15.2/go.mod h1:Epx+iuz8sNs7mNKhxzH4fWXGNpZwUaJKRS1n github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e/go.mod h1:zD1mROLANZcx1PVRCS0qkT7pwLkGfwJo4zjcN/Tysno= github.com/oapi-codegen/runtime v1.1.1 h1:EXLHh0DXIJnWhdRPN2w4MXAzFyE4CskzhNLUmtpMYro= github.com/oapi-codegen/runtime v1.1.1/go.mod h1:SK9X900oXmPWilYR5/WKPzt3Kqxn/uS/+lbpREv+eCg= -github.com/pelletier/go-toml/v2 v2.2.2 h1:aYUidT7k73Pcl9nb2gScu7NSrKCSHIDE89b3+6Wq+LM= -github.com/pelletier/go-toml/v2 v2.2.2/go.mod h1:1t835xjRzz80PqgE6HHgN2JOsmgYu/h4qDAS4n929Rs= +github.com/pelletier/go-toml/v2 v2.2.3 h1:YmeHyLY8mFWbdkNWwpr+qIL2bEqT0o95WSdkNHvL12M= +github.com/pelletier/go-toml/v2 v2.2.3/go.mod h1:MfCQTFTvCcUyyvvwm1+G6H/jORL20Xlb6rzQu9GuUkc= github.com/perimeterx/marshmallow v1.1.4 h1:pZLDH9RjlLGGorbXhcaQLhfuV0pFMNfPO55FuFkxqLw= github.com/perimeterx/marshmallow v1.1.4/go.mod h1:dsXbUu8CRzfYP5a87xpp0xq9S3u0Vchtcl8we9tYaXw= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= @@ -157,7 +156,6 @@ github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/ github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4= -github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo= github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg= github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= github.com/ugorji/go v1.2.7 h1:qYhyWUUd6WbiM+C6JZAUkIJt/1WrjzNHY9+KCIjVqTo= @@ -169,27 +167,25 @@ github.com/vmihailenco/msgpack/v5 v5.3.5 h1:5gO0H1iULLWGhs2H5tbAHIZTV8/cYafcFOr9 github.com/vmihailenco/msgpack/v5 v5.3.5/go.mod h1:7xyJ9e+0+9SaZT0Wt1RGleJXzli6Q/V5KbhBonMG9jc= github.com/vmihailenco/tagparser/v2 v2.0.0 h1:y09buUbR+b5aycVFQs/g70pqKVZNBmxwAhO7/IwNM9g= github.com/vmihailenco/tagparser/v2 v2.0.0/go.mod h1:Wri+At7QHww0WTrCBeu4J6bNtoV6mEfg5OIWRZA9qds= -github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e h1:JVG44RsyaB9T2KIHavMF/ppJZNG9ZpyihvCd0w101no= -github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e/go.mod h1:RbqR21r5mrJuqunuUZ/Dhy/avygyECGrLceyNeo4LiM= github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY= github.com/zalando/go-keyring v0.2.5 h1:Bc2HHpjALryKD62ppdEzaFG6VxL6Bc+5v0LYpN8Lba8= github.com/zalando/go-keyring v0.2.5/go.mod h1:HL4k+OXQfJUWaMnqyuSOc0drfGPX2b51Du6K+MRgZMk= golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= -golang.org/x/crypto v0.26.0 h1:RrRspgV4mU+YwB4FYnuBoKsUapNIL5cohGAmSH3azsw= -golang.org/x/crypto v0.26.0/go.mod h1:GY7jblb9wI+FOo5y8/S2oY4zWP07AkOJ4+jxCqdqn54= +golang.org/x/crypto v0.28.0 h1:GBDwsMXVQi34v5CCYUm2jkJvu4cbtru2U4TN2PSyQnw= +golang.org/x/crypto v0.28.0/go.mod h1:rmgy+3RHxRZMyY0jjAJShp2zgEdOqj2AO7U0pYmeQ7U= golang.org/x/exp v0.0.0-20240318143956-a85f2c67cd81 h1:6R2FC06FonbXQ8pK11/PDFY6N6LWlf9KlzibaCapmqc= golang.org/x/exp v0.0.0-20240318143956-a85f2c67cd81/go.mod h1:CQ1k9gNrJ50XIzaKCRR2hssIjF07kZFEiieALBM/ARQ= golang.org/x/lint v0.0.0-20210508222113-6edffad5e616 h1:VLliZ0d+/avPrXXH+OakdXhpJuEoBZuwh1m2j7U6Iug= golang.org/x/lint v0.0.0-20210508222113-6edffad5e616/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY= golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4= -golang.org/x/mod v0.20.0 h1:utOm6MM3R3dnawAiJgn0y+xvuYRsm1RKM/4giyfDgV0= -golang.org/x/mod v0.20.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c= +golang.org/x/mod v0.21.0 h1:vvrHzRwRfVKSiLrG+d4FMl/Qi4ukBCE6kZlTUkDYRT0= +golang.org/x/mod v0.21.0/go.mod h1:6SkKJ3Xj0I0BrPOZoBy3bdMptDDU9oJrpohJ3eWZ1fY= golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c= -golang.org/x/net v0.28.0 h1:a9JDOJc5GMUJ0+UDqmLT86WiEy7iWyIhz8gz8E4e5hE= -golang.org/x/net v0.28.0/go.mod h1:yqtgsTWOOnlGLG9GFRrK3++bGOUEkNBoHZc8MEDWPNg= +golang.org/x/net v0.30.0 h1:AcW1SDZMkb8IpzCdQUaIq2sP4sZ4zw+55h6ynffypl4= +golang.org/x/net v0.30.0/go.mod h1:2wGyMJ5iFasEhkwi13ChkO/t1ECNC4X4eBKkVFyYFlU= golang.org/x/oauth2 v0.18.0 h1:09qnuIAgzdx1XplqJvW6CQqMCtGZykZWcXzPMPUusvI= golang.org/x/oauth2 v0.18.0/go.mod h1:Wf7knwG0MPoWIMMBgFlEaSUDaKskp0dCfrlJRJXbBi8= golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= @@ -204,16 +200,16 @@ golang.org/x/sys v0.0.0-20211007075335-d3039528d8ac/go.mod h1:oPkhp1MJrh7nUepCBc golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.24.0 h1:Twjiwq9dn6R1fQcyiK+wQyHWfaz/BJB+YIpzU/Cv3Xg= -golang.org/x/sys v0.24.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= +golang.org/x/sys v0.26.0 h1:KHjCJyddX0LoSTb3J+vWpupP9p0oznkqVk/IfjymZbo= +golang.org/x/sys v0.26.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= golang.org/x/text v0.3.8/go.mod h1:E6s5w1FMmriuDzIBO73fBruAKo1PCIq6d2Q6DHfQ8WQ= -golang.org/x/text v0.17.0 h1:XtiM5bkSOt+ewxlOE/aE/AKEHibwj/6gvWMl9Rsh0Qc= -golang.org/x/text v0.17.0/go.mod h1:BuEKDfySbSR4drPmRPG/7iBdf8hvFMuRexcpahXilzY= +golang.org/x/text v0.19.0 h1:kTxAhCbGbxhK0IwgSKiMO5awPoDQ0RpfiVYBfK860YM= +golang.org/x/text v0.19.0/go.mod h1:BuEKDfySbSR4drPmRPG/7iBdf8hvFMuRexcpahXilzY= golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc= diff --git a/lcl/lcl.go b/lcl/lcl.go index a9e73a4..9bbf57f 100644 --- a/lcl/lcl.go +++ b/lcl/lcl.go @@ -2,15 +2,9 @@ package lcl import ( "context" - "crypto/tls" - "encoding/base64" "fmt" "net" "slices" - "time" - - "golang.org/x/crypto/acme" - "golang.org/x/crypto/acme/autocert" "github.com/anchordotdev/cli" "github.com/anchordotdev/cli/api" @@ -255,35 +249,6 @@ func (c *Command) realmAPID(ctx context.Context, cfg *cli.Config, drv *ui.Driver return realm.Apid, nil } -func provisionCert(eab *api.Eab, domains []string, acmeURL string) (*tls.Certificate, error) { - hmacKey, err := base64.URLEncoding.DecodeString(eab.HmacKey) - if err != nil { - return nil, err - } - - mgr := &autocert.Manager{ - Prompt: autocert.AcceptTOS, - HostPolicy: autocert.HostWhitelist(domains...), - Client: &acme.Client{ - DirectoryURL: acmeURL, - UserAgent: cli.UserAgent(), - }, - ExternalAccountBinding: &acme.ExternalAccountBinding{ - KID: eab.Kid, - Key: hmacKey, - }, - RenewBefore: 24 * time.Hour, - } - - // TODO: switch to using ACME package here, so that extra domains can be sent through for SAN extension - clientHello := &tls.ClientHelloInfo{ - ServerName: domains[0], - CipherSuites: []uint16{tls.TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256}, - } - - return mgr.GetCertificate(clientHello) -} - func checkLoopbackDomain(ctx context.Context, drv *ui.Driver, domain string) error { drv.Activate(ctx, &models.DomainResolver{ Domain: domain, diff --git a/lcl/lcl_test.go b/lcl/lcl_test.go index 343838e..e3d30dd 100644 --- a/lcl/lcl_test.go +++ b/lcl/lcl_test.go @@ -126,7 +126,6 @@ func TestLcl(t *testing.T) { cfg.Trust.MockMode = true cfg.Trust.NoSudo = true cfg.Trust.Stores = []string{"mock"} - cfg.Test.LclHostPort = 4321 cfg.Test.Prefer = map[string]cli.ConfigTestPrefer{ "/v0/orgs": { Example: "anky_personal", @@ -246,6 +245,19 @@ func TestLcl(t *testing.T) { fmt.Sprintf("! Press Enter to open %s in your browser.", setupGuideURL), ) + anc, err := api.NewClient(ctx, cfg) + if err != nil { + t.Fatal(err) + } + + srv, err := anc.GetService(ctx, "lcl", "test-app") + if err != nil { + t.Fatal(err) + } + + lclUrl := fmt.Sprintf("https://test-app.lcl.host:%d", *srv.LocalhostPort) + drv.Replace(lclUrl, "https://test-app.lcl.host:") + tm.Send(tea.KeyMsg{Type: tea.KeyEnter}) tm.WaitFinished(t, teatest.WithFinalTimeout(time.Second*3)) diff --git a/lcl/mkcert.go b/lcl/mkcert.go index 8cfeccb..22a63b3 100644 --- a/lcl/mkcert.go +++ b/lcl/mkcert.go @@ -134,7 +134,7 @@ func (c *MkCert) perform(ctx context.Context, cfg *cli.Config, drv *ui.Driver) ( acmeURL := cfg.AcmeURL(orgAPID, realmAPID, chainAPID) - tlsCert, err := provisionCert(c.eab, domains, acmeURL) + tlsCert, err := api.ProvisionCert(c.eab, domains, acmeURL) if err != nil { return nil, err } diff --git a/lcl/setup_test.go b/lcl/setup_test.go index 245ebcc..f85cb39 100644 --- a/lcl/setup_test.go +++ b/lcl/setup_test.go @@ -6,14 +6,16 @@ import ( "testing" "time" + tea "github.com/charmbracelet/bubbletea" + "github.com/charmbracelet/x/exp/teatest" + "github.com/stretchr/testify/require" + "github.com/anchordotdev/cli" + "github.com/anchordotdev/cli/api" "github.com/anchordotdev/cli/clipboard" "github.com/anchordotdev/cli/cmdtest" "github.com/anchordotdev/cli/truststore" "github.com/anchordotdev/cli/ui/uitest" - tea "github.com/charmbracelet/bubbletea" - "github.com/charmbracelet/x/exp/teatest" - "github.com/stretchr/testify/require" ) func TestCmdLclSetup(t *testing.T) { @@ -58,7 +60,6 @@ func TestSetup(t *testing.T) { cfg := cmdtest.Config(ctx) cfg.API.URL = srv.URL cfg.Test.ACME.URL = "http://anchor.lcl.host:" + srv.RailsPort - cfg.Test.LclHostPort = 4321 cfg.Trust.MockMode = true cfg.Trust.NoSudo = true cfg.Trust.Stores = []string{"mock"} @@ -121,6 +122,22 @@ func TestSetup(t *testing.T) { fmt.Sprintf("! Press Enter to open %s in your browser.", setupGuideURL), ) + { + anc, err := api.NewClient(ctx, cfg) + if err != nil { + t.Fatal(err) + } + + srv, err := anc.GetService(ctx, "lcl_setup", "test-app") + if err != nil { + t.Fatal(err) + } + + lclUrl := fmt.Sprintf("https://test-app.lcl.host:%d", *srv.LocalhostPort) + + drv.Replace(lclUrl, "https://test-app.lcl.host:") + } + tm.Send(tea.KeyMsg{Type: tea.KeyEnter}) tm.WaitFinished(t, teatest.WithFinalTimeout(time.Second*3)) diff --git a/lcl/testdata/TestLcl/basics.golden b/lcl/testdata/TestLcl/basics.golden index 7701946..8e02a5b 100644 --- a/lcl/testdata/TestLcl/basics.golden +++ b/lcl/testdata/TestLcl/basics.golden @@ -1741,7 +1741,7 @@ - Opened https://anchor.dev/lcl/services/test-app/guide. # Next Steps - - After following the guide, check out your encrypted site at: https://test-app.lcl.host:4321 + - After following the guide, check out your encrypted site at: https://test-app.lcl.host: - These certificates will renew automatically, time to enjoy effortless encryption. ─── AnchorTOML ───────────────────────────────────────────────────────────────── | Let's set up fast and totally free lcl.host HTTPS! @@ -1786,6 +1786,6 @@ - Opened https://anchor.dev/lcl/services/test-app/guide. # Next Steps - - After following the guide, check out your encrypted site at: https://test-app.lcl.host:4321 + - After following the guide, check out your encrypted site at: https://test-app.lcl.host: - These certificates will renew automatically, time to enjoy effortless encryption. - Be sure to add anchor.toml to your version control system. diff --git a/lcl/testdata/TestSetup/create-service-automated-basics.golden b/lcl/testdata/TestSetup/create-service-automated-basics.golden index 2534d2d..7067b78 100644 --- a/lcl/testdata/TestSetup/create-service-automated-basics.golden +++ b/lcl/testdata/TestSetup/create-service-automated-basics.golden @@ -357,7 +357,7 @@ - Opened https://anchor.dev/lcl_setup/services/test-app/guide. # Next Steps - - After following the guide, check out your encrypted site at: https://test-app.lcl.host:4321 + - After following the guide, check out your encrypted site at: https://test-app.lcl.host: - These certificates will renew automatically, time to enjoy effortless encryption. ─── AnchorTOML ───────────────────────────────────────────────────────────────── @@ -379,6 +379,6 @@ - Opened https://anchor.dev/lcl_setup/services/test-app/guide. # Next Steps - - After following the guide, check out your encrypted site at: https://test-app.lcl.host:4321 + - After following the guide, check out your encrypted site at: https://test-app.lcl.host: - These certificates will renew automatically, time to enjoy effortless encryption. - Be sure to add anchor.toml to your version control system. diff --git a/models/cli.go b/models/cli.go index 6099969..22436ef 100644 --- a/models/cli.go +++ b/models/cli.go @@ -75,15 +75,3 @@ func (m *Browserless) View() string { return b.String() } - -func ConfigLoaded(path string) tea.Model { - return ui.Section{ - Name: "ConfigFileLoaded", - Model: ui.MessageFunc(func(b *strings.Builder) { - fmt.Fprintln(b, ui.StepDone(fmt.Sprintf("Loaded %s configuration. %s", - ui.Emphasize(path), - ui.Whisper("You can use `--skip-config` to ignore this file."), - ))) - }), - } -} diff --git a/service/env_test.go b/service/env_test.go index a8f5849..87879c1 100644 --- a/service/env_test.go +++ b/service/env_test.go @@ -2,7 +2,6 @@ package service import ( "context" - "os" "testing" "time" @@ -11,27 +10,11 @@ import ( "github.com/stretchr/testify/require" "github.com/anchordotdev/cli" - "github.com/anchordotdev/cli/api/apitest" "github.com/anchordotdev/cli/clipboard" "github.com/anchordotdev/cli/cmdtest" "github.com/anchordotdev/cli/ui/uitest" ) -var srv = &apitest.Server{ - Host: "api.anchor.lcl.host", - RootDir: "../..", -} - -func TestMain(m *testing.M) { - if err := srv.Start(context.Background()); err != nil { - panic(err) - } - - defer os.Exit(m.Run()) - - srv.Close() -} - func TestCmdServiceEnv(t *testing.T) { t.Run("--help", func(t *testing.T) { cmdtest.TestHelp(t, CmdServiceEnv, "service", "env", "--help") diff --git a/service/models/probe.go b/service/models/probe.go new file mode 100644 index 0000000..5e9179f --- /dev/null +++ b/service/models/probe.go @@ -0,0 +1,94 @@ +package models + +import ( + "fmt" + "strings" + + "github.com/anchordotdev/cli/ui" + "github.com/charmbracelet/bubbles/spinner" + tea "github.com/charmbracelet/bubbletea" +) + +var ( + ProbeHeader = ui.Section{ + Name: "ProbeHeader", + Model: ui.MessageLines{ + ui.Header(fmt.Sprintf("Probe Service TLS Setup and Configuration %s", ui.Whisper("`anchor service probe`"))), + }, + } + + ProbeHint = ui.Section{ + Name: "ProbeHint", + Model: ui.MessageLines{ + ui.StepHint("We'll check your running app to ensure TLS works as expected."), + }, + } +) + +type Checker struct { + Name string + + err error + finished bool + + spinner spinner.Model +} + +func (m *Checker) Init() tea.Cmd { + m.spinner = ui.WaitingSpinner() + + return m.spinner.Tick +} + +type checkerMsg struct { + mdl *Checker + err error +} + +func (m *Checker) Pass() tea.Msg { + return checkerMsg{ + mdl: m, + err: nil, + } +} + +func (m *Checker) Fail(err error) tea.Msg { + return checkerMsg{ + mdl: m, + err: err, + } +} + +func (m *Checker) Update(msg tea.Msg) (tea.Model, tea.Cmd) { + switch msg := msg.(type) { + case checkerMsg: + if msg.mdl == m { + m.finished = true + m.err = msg.err + } + return m, nil + default: + var cmd tea.Cmd + m.spinner, cmd = m.spinner.Update(msg) + return m, cmd + } +} + +func (m *Checker) View() string { + var b strings.Builder + switch { + case !m.finished: + fmt.Fprintln(&b, ui.StepInProgress(fmt.Sprintf("Checking %s…%s", + m.Name, + m.spinner.View()))) + case m.err == nil: + fmt.Fprintln(&b, ui.StepDone(fmt.Sprintf("Checked %s: success!", + m.Name))) + default: + fmt.Fprintln(&b, ui.StepAlert(fmt.Sprintf("Checked %s: failed!", + m.Name))) + fmt.Fprintln(&b, ui.StepAlert(fmt.Sprintf("Error! %s", + m.err.Error()))) + } + return b.String() +} diff --git a/service/probe.go b/service/probe.go new file mode 100644 index 0000000..65c102e --- /dev/null +++ b/service/probe.go @@ -0,0 +1,335 @@ +package service + +import ( + "context" + "crypto/tls" + "crypto/x509" + "fmt" + "net" + "slices" + "strconv" + "strings" + "time" + + "github.com/spf13/cobra" + + "github.com/anchordotdev/cli" + "github.com/anchordotdev/cli/api" + "github.com/anchordotdev/cli/auth" + "github.com/anchordotdev/cli/component" + componentmodels "github.com/anchordotdev/cli/component/models" + "github.com/anchordotdev/cli/service/models" + "github.com/anchordotdev/cli/ui" +) + +var cmdServiceProbe = cli.NewCmd[Probe](CmdService, "probe", func(cmd *cobra.Command) { + cfg := cli.ConfigFromCmd(cmd) + + cmd.Flags().StringVarP(&cfg.Org.APID, "org", "o", cli.Defaults.Org.APID, "Organization of the service to probe.") + cmd.Flags().StringVarP(&cfg.Realm.APID, "realm", "r", cli.Defaults.Realm.APID, "Realm instance of the service to probe.") + cmd.Flags().StringVarP(&cfg.Service.APID, "service", "s", cli.Defaults.Service.APID, "Service to probe.") + + cmd.Flags().DurationVar(&cfg.Service.Probe.Timeout, "timeout", cli.Defaults.Service.Probe.Timeout, "Time to wait for a successful probe of the service.") +}) + +type Probe struct { + anc *api.Session + + OrgAPID, RealmAPID, ServiceAPID string +} + +func (c Probe) UI() cli.UI { + return cli.UI{ + RunTUI: c.RunTUI, + } +} + +func (c *Probe) RunTUI(ctx context.Context, drv *ui.Driver) error { + cfg := cli.ConfigFromContext(ctx) + + var err error + cmd := &auth.Client{ + Anc: c.anc, + } + + drv.Activate(ctx, models.ProbeHeader) + drv.Activate(ctx, models.ProbeHint) + + c.anc, err = cmd.Perform(ctx, drv) + if err != nil { + return err + } + + orgAPID, err := c.orgAPID(ctx, cfg, drv) + if err != nil { + return err + } + + serviceAPID, err := c.serviceAPID(ctx, cfg, drv, orgAPID) + if err != nil { + return err + } + + // TODO: show "fetching" model + attachments, err := c.anc.GetServiceAttachments(ctx, orgAPID, serviceAPID) + if err != nil { + return err + } + + realmAPID, err := c.realmAPID(ctx, cfg, drv, orgAPID, serviceAPID, attachments) + if err != nil { + return err + } + + idx := slices.IndexFunc(attachments, func(attachment api.Attachment) bool { + return attachment.Relationships.Realm.Apid == realmAPID + }) + if idx == -1 { + panic("no attachment for --realm") + } + attachment := attachments[idx] + + // TODO: show "fetching" model + + credentials, err := c.anc.GetCredentials(ctx, orgAPID, realmAPID, api.SubCA(*attachment.Relationships.SubCa.Apid)) + if err != nil { + return err + } + + ctx, cancel := context.WithTimeout(ctx, cfg.Service.Probe.Timeout) + defer cancel() + + addrs, err := c.probeDNS(ctx, cfg, drv, attachment) + if err != nil { + return nil + } + + conn, err := c.probeTCP(ctx, cfg, drv, attachment, addrs) + if err != nil { + return nil + } + + c.probeTLS(ctx, drv, attachment, credentials, conn) + return nil +} + +func (c *Probe) probeDNS(ctx context.Context, cfg *cli.Config, drv *ui.Driver, attachment api.Attachment) ([]string, error) { + resolver := cfg.Test.NetResolver + if resolver == nil { + resolver = new(net.Resolver) + } + + domain := attachment.Domains[0] + + mdl := &models.Checker{ + Name: fmt.Sprintf("DNS %s", ui.Emphasize(domain)), + } + drv.Activate(ctx, mdl) + + ips, err := resolver.LookupHost(context.Background(), domain) + if err != nil { + drv.Send(mdl.Fail(err)) + return nil, err + } + + drv.Send(mdl.Pass()) + return ips, nil +} + +func (c *Probe) probeTCP(ctx context.Context, cfg *cli.Config, drv *ui.Driver, attachment api.Attachment, ips []string) (net.Conn, error) { + ctx, cancel := context.WithCancel(ctx) + defer cancel() + + innerDialer := cfg.Test.NetDialer + if innerDialer == nil { + innerDialer = new(net.Dialer) + } + + resolver := cfg.Test.NetResolver + if resolver == nil { + resolver = new(net.Resolver) + } + + var addrs []string + for _, ip := range ips { + if strings.Contains(ip, ":") { + ip = "[" + ip + "]" + } + addr := ip + ":" + strconv.Itoa(int(attachment.Port)) + addrs = append(addrs, addr) + } + + mdl := &models.Checker{ + Name: fmt.Sprintf("TCP [%s]", ui.Emphasize(strings.Join(addrs, ", "))), + } + drv.Activate(ctx, mdl) + + connc := make(chan net.Conn) + for _, addr := range addrs { + go dialer{ + inner: innerDialer, + addr: addr, + connc: connc, + backoff: 1 * time.Second, + }.run(ctx) + } + + select { + case conn := <-connc: + drv.Send(mdl.Pass()) + return conn, nil + case <-ctx.Done(): + drv.Send(mdl.Fail(ctx.Err())) + return nil, ctx.Err() + } +} + +func (c *Probe) probeTLS(ctx context.Context, drv *ui.Driver, attachment api.Attachment, credentials []api.Credential, conn net.Conn) error { + domain := attachment.Domains[0] + + mdl := &models.Checker{ + Name: fmt.Sprintf("TLS %s", ui.Emphasize(domain)), + } + drv.Activate(ctx, mdl) + + subCAs := x509.NewCertPool() + for _, cred := range credentials { + subCAs.AppendCertsFromPEM([]byte(cred.TextualEncoding)) + } + + cfg := &tls.Config{ + ServerName: domain, + RootCAs: subCAs, + } + + if err := tls.Client(conn, cfg).HandshakeContext(ctx); err != nil { + drv.Send(mdl.Fail(err)) + return err + } + + drv.Send(mdl.Pass()) + return nil +} + +func (c *Probe) orgAPID(ctx context.Context, cfg *cli.Config, drv *ui.Driver) (string, error) { + if c.OrgAPID != "" { + return c.OrgAPID, nil + } + + if cfg.Org.APID != "" { + drv.Activate(ctx, &componentmodels.ConfigVia{ + Config: cfg, + ConfigFetchFn: func(cfg *cli.Config) any { return cfg.Org.APID }, + Flag: "--org", + Singular: "organization", + }) + c.OrgAPID = cfg.Org.APID + return c.OrgAPID, nil + } + + selector := &component.Selector[api.Organization]{ + Prompt: "What is the organization of the service you want to probe?", + Flag: "--org", + + Fetcher: &component.Fetcher[api.Organization]{ + FetchFn: func() ([]api.Organization, error) { return c.anc.GetOrgs(ctx) }, + }, + } + + org, err := selector.Choice(ctx, drv) + if err != nil { + return "", err + } + return org.Apid, nil +} + +func (c *Probe) realmAPID(ctx context.Context, cfg *cli.Config, drv *ui.Driver, orgAPID, serviceAPID string, attachments []api.Attachment) (string, error) { + if c.RealmAPID == "" && cfg.Lcl.RealmAPID != "" { + drv.Activate(ctx, &componentmodels.ConfigVia{ + Config: cfg, + ConfigFetchFn: func(cfg *cli.Config) any { return cfg.Lcl.RealmAPID }, + Flag: "--realm", + Singular: "realm", + }) + c.RealmAPID = cfg.Lcl.RealmAPID + } + + if c.RealmAPID == "" { + selector := &component.Selector[api.Realm]{ + Prompt: fmt.Sprintf("Which realm of the %s/%s service do you want to probe?", ui.Emphasize(orgAPID), ui.Emphasize(serviceAPID)), + Flag: "--realm", + + Fetcher: &component.Fetcher[api.Realm]{ + // TODO: filter GetOrgRealms results to just attachedRealms + FetchFn: func() ([]api.Realm, error) { return c.anc.GetOrgRealms(ctx, orgAPID) }, + }, + } + + realm, err := selector.Choice(ctx, drv) + if err != nil { + return "", err + } + c.RealmAPID = realm.Apid + } + + var attachedRealms []string + for _, attachment := range attachments { + attachedRealms = append(attachedRealms, attachment.Relationships.Realm.Apid) + } + + if slices.Contains(attachedRealms, c.RealmAPID) { + return c.RealmAPID, nil + } + return "", fmt.Errorf("%s service is not attached to the %s/%s realm", serviceAPID, orgAPID, c.RealmAPID) +} + +func (c *Probe) serviceAPID(ctx context.Context, cfg *cli.Config, drv *ui.Driver, orgAPID string) (string, error) { + if c.ServiceAPID != "" { + return c.ServiceAPID, nil + } + + if cfg.Service.APID != "" { + drv.Activate(ctx, &componentmodels.ConfigVia{ + Config: cfg, + ConfigFetchFn: func(cfg *cli.Config) any { return cfg.Service.APID }, + Flag: "--service", + Singular: "service", + }) + c.ServiceAPID = cfg.Service.APID + return c.ServiceAPID, nil + } + + selector := &component.Selector[api.Service]{ + Prompt: fmt.Sprintf("Which %s service do you want to probe?", ui.Emphasize(orgAPID)), + Flag: "--service", + + Fetcher: &component.Fetcher[api.Service]{ + FetchFn: func() ([]api.Service, error) { return c.anc.GetOrgServices(ctx, orgAPID) }, + }, + } + + service, err := selector.Choice(ctx, drv) + if err != nil { + return "", err + } + return service.Slug, nil +} + +type dialer struct { + inner cli.Dialer + addr string + connc chan<- net.Conn + backoff time.Duration +} + +func (d dialer) run(ctx context.Context) { + for { + conn, err := d.inner.DialContext(ctx, "tcp", d.addr) + if err == nil { + d.connc <- conn + return + } + + time.Sleep(d.backoff) + } +} diff --git a/service/probe_test.go b/service/probe_test.go new file mode 100644 index 0000000..11300ba --- /dev/null +++ b/service/probe_test.go @@ -0,0 +1,352 @@ +package service + +import ( + "context" + "crypto/tls" + "fmt" + "net" + "net/http" + "net/http/httptest" + "testing" + "time" + + "github.com/anchordotdev/cli" + "github.com/anchordotdev/cli/api" + "github.com/anchordotdev/cli/cmdtest" + _ "github.com/anchordotdev/cli/testflags" + "github.com/anchordotdev/cli/ui/uitest" + "github.com/benburkert/dns" + "github.com/charmbracelet/x/exp/teatest" +) + +func TestCmdServiceProbe(t *testing.T) { + t.Run("--help", func(t *testing.T) { + cmdtest.TestHelp(t, cmdServiceProbe, "service", "probe", "--help") + }) +} + +func TestProbe(t *testing.T) { + if !srv.IsProxy() { + t.Skip("service probe errors unsupported in non-proxy mode") + } + + ctx, cancel := context.WithCancel(context.Background()) + defer cancel() + + apiToken, err := srv.GeneratePAT("anky@anchor.dev") + if err != nil { + t.Fatal(err) + } + + cfg := cmdtest.Config(ctx) + cfg.API.Token = apiToken + cfg.API.URL = srv.URL + cfg.Dashboard.URL = "http://anchor.lcl.host:" + srv.RailsPort + cfg.Service.Probe.Timeout = 3 * time.Second + + anc, err := api.NewClient(ctx, cfg) + if err != nil { + t.Fatal(err) + } + + attachments, err := anc.GetServiceAttachments(ctx, "ankydotdev", "ankydotdev") + if err != nil { + t.Fatal(err) + } + if want, got := 1, len(attachments); want != got { + t.Fatalf("want %d ankydotdev service attachments, got %d", want, got) + } + + eab, err := anc.CreateEAB(ctx, "ca", "ankydotdev", "localhost", "ankydotdev", *attachments[0].Relationships.SubCa.Apid) + if err != nil { + t.Fatal(err) + } + + acmeURL := cfg.AcmeURL("ankydotdev", "localhost", attachments[0].Relationships.Chain.Apid) + + tlsCert, err := api.ProvisionCert(eab, []string{"ankydotdev.lcl.host"}, acmeURL) + if err != nil { + t.Fatal(err) + } + + srvHTTPS := httptest.NewUnstartedServer(http.NotFoundHandler()) + srvHTTPS.TLS = &tls.Config{ + Certificates: []tls.Certificate{*tlsCert}, + } + srvHTTPS.StartTLS() + defer srvHTTPS.Close() + + cfg.Test.NetDialer = cli.DialFunc(func(ctx context.Context, network, address string) (net.Conn, error) { + return new(net.Dialer).DialContext(ctx, network, srvHTTPS.Listener.Addr().String()) + }) + + ctx = cli.ContextWithConfig(ctx, cfg) + + cmd := &Probe{ + anc: anc, + } + + drv, tm := uitest.TestTUI(ctx, t) + drv.Replace(fmt.Sprintf("[::1]:%d", attachments[0].Port), "[::1]:") + drv.Replace(fmt.Sprintf("127.0.0.1:%d", attachments[0].Port), "127.0.0.1:") + + errc := make(chan error, 1) + go func() { + errc <- cmd.UI().RunTUI(ctx, drv) + errc <- tm.Quit() + }() + + tm.WaitFinished(t, teatest.WithFinalTimeout(time.Second*2)) + uitest.TestGolden(t, drv.Golden()) +} + +func TestProbeErrors(t *testing.T) { + if srv.IsProxy() { + t.Skip("service probe errors unsupported in proxy mode") + } + + apiToken, err := srv.GeneratePAT("anky@anchor.dev") + if err != nil { + t.Fatal(err) + } + + setup := func(ctx context.Context) (*cli.Config, *api.Session, error) { + cfg := cmdtest.Config(ctx) + cfg.API.Token = apiToken + cfg.API.URL = srv.URL + cfg.Service.Probe.Timeout = 2 * time.Second + + anc, err := api.NewClient(ctx, cfg) + return cfg, anc, err + } + + t.Run("dns-failure", func(t *testing.T) { + ctx, cancel := context.WithCancel(context.Background()) + defer cancel() + + dnsZone := &dns.Zone{ + Origin: ".", + TTL: 1 * time.Minute, + RRs: dns.RRSet{}, + } + + dnsMux := new(dns.ResolveMux) + dnsMux.Handle(dns.TypeANY, dnsZone.Origin, dnsZone) + + dnsClient := &dns.Client{ + Resolver: dnsMux, + } + + resolver := &net.Resolver{ + PreferGo: true, + Dial: dnsClient.Dial, + } + + cfg, anc, err := setup(ctx) + if err != nil { + t.Fatal(err) + } + cfg.Test.NetResolver = resolver + + ctx = cli.ContextWithConfig(ctx, cfg) + + cmd := &Probe{ + anc: anc, + } + + drv, tm := uitest.TestTUI(ctx, t) + + if _, err = resolver.LookupHost(ctx, "service.lcl.host"); err == nil { + t.Fatal("expected DNS lookup error, got none") + } + drv.Replace(err.Error(), "") + + errc := make(chan error, 1) + go func() { + errc <- cmd.UI().RunTUI(ctx, drv) + errc <- tm.Quit() + }() + + tm.WaitFinished(t, teatest.WithFinalTimeout(time.Second*2)) + uitest.TestGolden(t, drv.Golden()) + }) + + t.Run("tcp-failure-connection-timeout", func(t *testing.T) { + ctx, cancel := context.WithCancel(context.Background()) + defer cancel() + + dialer := cli.DialFunc(func(ctx context.Context, network, address string) (net.Conn, error) { + return nil, context.DeadlineExceeded + }) + + cfg, anc, err := setup(ctx) + if err != nil { + t.Fatal(err) + } + cfg.Test.NetDialer = dialer + cfg.Service.Probe.Timeout = 100 * time.Millisecond + + ctx = cli.ContextWithConfig(ctx, cfg) + + cmd := &Probe{ + anc: anc, + } + + drv, tm := uitest.TestTUI(ctx, t) + drv.Replace("[::1]:44344, ", "") + drv.Replace("127.0.0.1:44344", "") + + errc := make(chan error, 1) + go func() { + errc <- cmd.UI().RunTUI(ctx, drv) + errc <- tm.Quit() + }() + + tm.WaitFinished(t, teatest.WithFinalTimeout(time.Second*2)) + uitest.TestGolden(t, drv.Golden()) + }) + + t.Run("tcp-failure-connection-failure", func(t *testing.T) { + ctx, cancel := context.WithCancel(context.Background()) + defer cancel() + + ln, err := net.Listen("tcp", ":0") + if err != nil { + t.Fatal(err) + } + + ln.Close() // close right so dial fails + + dialer := cli.DialFunc(func(ctx context.Context, network, address string) (net.Conn, error) { + host, _, err := net.SplitHostPort(address) + if err != nil { + return nil, err + } + + _, port, err := net.SplitHostPort(ln.Addr().String()) + if err != nil { + return nil, err + } + + return new(net.Dialer).DialContext(ctx, network, host+":"+port) + }) + + cfg, anc, err := setup(ctx) + if err != nil { + t.Fatal(err) + } + cfg.Test.NetDialer = dialer + cfg.Service.Probe.Timeout = 100 * time.Millisecond + + ctx = cli.ContextWithConfig(ctx, cfg) + + cmd := &Probe{ + anc: anc, + } + + drv, tm := uitest.TestTUI(ctx, t) + drv.Replace("[::1]:44344, ", "") + drv.Replace("127.0.0.1:44344", "") + + errc := make(chan error, 1) + go func() { + errc <- cmd.UI().RunTUI(ctx, drv) + errc <- tm.Quit() + }() + + tm.WaitFinished(t, teatest.WithFinalTimeout(time.Second*2)) + uitest.TestGolden(t, drv.Golden()) + }) + + t.Run("tls-failure-non-tls-server", func(t *testing.T) { + ctx, cancel := context.WithCancel(context.Background()) + defer cancel() + + srvHTTP := httptest.NewServer(http.NotFoundHandler()) + + dialer := cli.DialFunc(func(ctx context.Context, network, address string) (net.Conn, error) { + host, _, err := net.SplitHostPort(address) + if err != nil { + return nil, err + } + + _, port, err := net.SplitHostPort(srvHTTP.Listener.Addr().String()) + if err != nil { + return nil, err + } + + return new(net.Dialer).DialContext(ctx, network, host+":"+port) + }) + + cfg, anc, err := setup(ctx) + if err != nil { + t.Fatal(err) + } + cfg.Test.NetDialer = dialer + + ctx = cli.ContextWithConfig(ctx, cfg) + + cmd := &Probe{ + anc: anc, + } + + drv, tm := uitest.TestTUI(ctx, t) + drv.Replace("[::1]:44344, ", "") + drv.Replace("127.0.0.1:44344", "") + + errc := make(chan error, 1) + go func() { + errc <- cmd.UI().RunTUI(ctx, drv) + errc <- tm.Quit() + }() + + tm.WaitFinished(t, teatest.WithFinalTimeout(time.Second*2)) + uitest.TestGolden(t, drv.Golden()) + }) + + t.Run("tls-failure-unknown-cert", func(t *testing.T) { + ctx, cancel := context.WithCancel(context.Background()) + defer cancel() + + srvHTTP := httptest.NewTLSServer(http.NotFoundHandler()) + + dialer := cli.DialFunc(func(ctx context.Context, network, address string) (net.Conn, error) { + host, _, err := net.SplitHostPort(address) + if err != nil { + return nil, err + } + + _, port, err := net.SplitHostPort(srvHTTP.Listener.Addr().String()) + if err != nil { + return nil, err + } + + return new(net.Dialer).DialContext(ctx, network, host+":"+port) + }) + + cfg, anc, err := setup(ctx) + if err != nil { + t.Fatal(err) + } + cfg.Test.NetDialer = dialer + + ctx = cli.ContextWithConfig(ctx, cfg) + + cmd := &Probe{ + anc: anc, + } + + drv, tm := uitest.TestTUI(ctx, t) + drv.Replace("[::1]:44344, ", "") + drv.Replace("127.0.0.1:44344", "") + + errc := make(chan error, 1) + go func() { + errc <- cmd.UI().RunTUI(ctx, drv) + errc <- tm.Quit() + }() + + tm.WaitFinished(t, teatest.WithFinalTimeout(time.Second*2)) + uitest.TestGolden(t, drv.Golden()) + }) +} diff --git a/service/service_test.go b/service/service_test.go new file mode 100644 index 0000000..de5cf5f --- /dev/null +++ b/service/service_test.go @@ -0,0 +1,25 @@ +package service + +import ( + "context" + "os" + "testing" + + "github.com/anchordotdev/cli/api/apitest" + _ "github.com/anchordotdev/cli/testflags" +) + +var srv = &apitest.Server{ + Host: "api.anchor.lcl.host", + RootDir: "../..", +} + +func TestMain(m *testing.M) { + if err := srv.Start(context.Background()); err != nil { + panic(err) + } + + defer os.Exit(m.Run()) + + srv.Close() +} diff --git a/service/testdata/TestCmdServiceProbe/--help.golden b/service/testdata/TestCmdServiceProbe/--help.golden new file mode 100644 index 0000000..04de171 --- /dev/null +++ b/service/testdata/TestCmdServiceProbe/--help.golden @@ -0,0 +1,16 @@ +Probe a service for proper TLS setup & configuration + +Usage: + anchor service probe [flags] + +Flags: + -h, --help help for probe + -o, --org string Organization of the service to probe. + -r, --realm string Realm instance of the service to probe. + -s, --service string Service to probe. + --timeout duration Time to wait for a successful probe of the service. (default 2m0s) + +Global Flags: + --api-token string Anchor API personal access token (PAT). + --config string Service configuration file. (default "anchor.toml") + --skip-config Skip loading configuration file. diff --git a/service/testdata/TestProbe.golden b/service/testdata/TestProbe.golden new file mode 100644 index 0000000..e99cb20 --- /dev/null +++ b/service/testdata/TestProbe.golden @@ -0,0 +1,111 @@ +─── ProbeHeader ──────────────────────────────────────────────────────────────── + +# Probe Service TLS Setup and Configuration `anchor service probe` +─── ProbeHint ────────────────────────────────────────────────────────────────── + +# Probe Service TLS Setup and Configuration `anchor service probe` + | We'll check your running app to ensure TLS works as expected. +─── Client ───────────────────────────────────────────────────────────────────── + +# Probe Service TLS Setup and Configuration `anchor service probe` + | We'll check your running app to ensure TLS works as expected. + * Checking authentication: probing credentials locally…⠋ +─── Client ───────────────────────────────────────────────────────────────────── + +# Probe Service TLS Setup and Configuration `anchor service probe` + | We'll check your running app to ensure TLS works as expected. + * Checking authentication: testing credentials remotely…⠋ +─── Client ───────────────────────────────────────────────────────────────────── + +# Probe Service TLS Setup and Configuration `anchor service probe` + | We'll check your running app to ensure TLS works as expected. +─── Fetcher[github.com/anchordotdev/cli/api.Organization] ────────────────────── + +# Probe Service TLS Setup and Configuration `anchor service probe` + | We'll check your running app to ensure TLS works as expected. + * Fetching organizations…⠋ +─── Fetcher[github.com/anchordotdev/cli/api.Organization] ────────────────────── + +# Probe Service TLS Setup and Configuration `anchor service probe` + | We'll check your running app to ensure TLS works as expected. + - Using ankydotdev, the only available organization. You can also use `--org ankydotdev`. +─── Fetcher[github.com/anchordotdev/cli/api.Service] ─────────────────────────── + +# Probe Service TLS Setup and Configuration `anchor service probe` + | We'll check your running app to ensure TLS works as expected. + - Using ankydotdev, the only available organization. You can also use `--org ankydotdev`. + * Fetching services…⠋ +─── Fetcher[github.com/anchordotdev/cli/api.Service] ─────────────────────────── + +# Probe Service TLS Setup and Configuration `anchor service probe` + | We'll check your running app to ensure TLS works as expected. + - Using ankydotdev, the only available organization. You can also use `--org ankydotdev`. + - Using ankydotdev, the only available service. You can also use `--service ankydotdev`. +─── Fetcher[github.com/anchordotdev/cli/api.Realm] ───────────────────────────── + +# Probe Service TLS Setup and Configuration `anchor service probe` + | We'll check your running app to ensure TLS works as expected. + - Using ankydotdev, the only available organization. You can also use `--org ankydotdev`. + - Using ankydotdev, the only available service. You can also use `--service ankydotdev`. + * Fetching realms…⠋ +─── Fetcher[github.com/anchordotdev/cli/api.Realm] ───────────────────────────── + +# Probe Service TLS Setup and Configuration `anchor service probe` + | We'll check your running app to ensure TLS works as expected. + - Using ankydotdev, the only available organization. You can also use `--org ankydotdev`. + - Using ankydotdev, the only available service. You can also use `--service ankydotdev`. + - Using localhost, the only available realm. You can also use `--realm localhost`. +─── Checker ──────────────────────────────────────────────────────────────────── + +# Probe Service TLS Setup and Configuration `anchor service probe` + | We'll check your running app to ensure TLS works as expected. + - Using ankydotdev, the only available organization. You can also use `--org ankydotdev`. + - Using ankydotdev, the only available service. You can also use `--service ankydotdev`. + - Using localhost, the only available realm. You can also use `--realm localhost`. + * Checking DNS ankydotdev.lcl.host…⠋ +─── Checker ──────────────────────────────────────────────────────────────────── + +# Probe Service TLS Setup and Configuration `anchor service probe` + | We'll check your running app to ensure TLS works as expected. + - Using ankydotdev, the only available organization. You can also use `--org ankydotdev`. + - Using ankydotdev, the only available service. You can also use `--service ankydotdev`. + - Using localhost, the only available realm. You can also use `--realm localhost`. + - Checked DNS ankydotdev.lcl.host: success! +─── Checker ──────────────────────────────────────────────────────────────────── + +# Probe Service TLS Setup and Configuration `anchor service probe` + | We'll check your running app to ensure TLS works as expected. + - Using ankydotdev, the only available organization. You can also use `--org ankydotdev`. + - Using ankydotdev, the only available service. You can also use `--service ankydotdev`. + - Using localhost, the only available realm. You can also use `--realm localhost`. + - Checked DNS ankydotdev.lcl.host: success! + * Checking TCP [[::1]:, 127.0.0.1:]…⠋ +─── Checker ──────────────────────────────────────────────────────────────────── + +# Probe Service TLS Setup and Configuration `anchor service probe` + | We'll check your running app to ensure TLS works as expected. + - Using ankydotdev, the only available organization. You can also use `--org ankydotdev`. + - Using ankydotdev, the only available service. You can also use `--service ankydotdev`. + - Using localhost, the only available realm. You can also use `--realm localhost`. + - Checked DNS ankydotdev.lcl.host: success! + - Checked TCP [[::1]:, 127.0.0.1:]: success! +─── Checker ──────────────────────────────────────────────────────────────────── + +# Probe Service TLS Setup and Configuration `anchor service probe` + | We'll check your running app to ensure TLS works as expected. + - Using ankydotdev, the only available organization. You can also use `--org ankydotdev`. + - Using ankydotdev, the only available service. You can also use `--service ankydotdev`. + - Using localhost, the only available realm. You can also use `--realm localhost`. + - Checked DNS ankydotdev.lcl.host: success! + - Checked TCP [[::1]:, 127.0.0.1:]: success! + * Checking TLS ankydotdev.lcl.host…⠋ +─── Checker ──────────────────────────────────────────────────────────────────── + +# Probe Service TLS Setup and Configuration `anchor service probe` + | We'll check your running app to ensure TLS works as expected. + - Using ankydotdev, the only available organization. You can also use `--org ankydotdev`. + - Using ankydotdev, the only available service. You can also use `--service ankydotdev`. + - Using localhost, the only available realm. You can also use `--realm localhost`. + - Checked DNS ankydotdev.lcl.host: success! + - Checked TCP [[::1]:, 127.0.0.1:]: success! + - Checked TLS ankydotdev.lcl.host: success! diff --git a/service/testdata/TestProbeErrors/dns-failure.golden b/service/testdata/TestProbeErrors/dns-failure.golden new file mode 100644 index 0000000..b7ea2a6 --- /dev/null +++ b/service/testdata/TestProbeErrors/dns-failure.golden @@ -0,0 +1,74 @@ +─── ProbeHeader ──────────────────────────────────────────────────────────────── + +# Probe Service TLS Setup and Configuration `anchor service probe` +─── ProbeHint ────────────────────────────────────────────────────────────────── + +# Probe Service TLS Setup and Configuration `anchor service probe` + | We'll check your running app to ensure TLS works as expected. +─── Client ───────────────────────────────────────────────────────────────────── + +# Probe Service TLS Setup and Configuration `anchor service probe` + | We'll check your running app to ensure TLS works as expected. + * Checking authentication: probing credentials locally…⠋ +─── Client ───────────────────────────────────────────────────────────────────── + +# Probe Service TLS Setup and Configuration `anchor service probe` + | We'll check your running app to ensure TLS works as expected. + * Checking authentication: testing credentials remotely…⠋ +─── Client ───────────────────────────────────────────────────────────────────── + +# Probe Service TLS Setup and Configuration `anchor service probe` + | We'll check your running app to ensure TLS works as expected. +─── Fetcher[github.com/anchordotdev/cli/api.Organization] ────────────────────── + +# Probe Service TLS Setup and Configuration `anchor service probe` + | We'll check your running app to ensure TLS works as expected. + * Fetching organizations…⠋ +─── Fetcher[github.com/anchordotdev/cli/api.Organization] ────────────────────── + +# Probe Service TLS Setup and Configuration `anchor service probe` + | We'll check your running app to ensure TLS works as expected. + - Using org-slug, the only available organization. You can also use `--org org-slug`. +─── Fetcher[github.com/anchordotdev/cli/api.Service] ─────────────────────────── + +# Probe Service TLS Setup and Configuration `anchor service probe` + | We'll check your running app to ensure TLS works as expected. + - Using org-slug, the only available organization. You can also use `--org org-slug`. + * Fetching services…⠋ +─── Fetcher[github.com/anchordotdev/cli/api.Service] ─────────────────────────── + +# Probe Service TLS Setup and Configuration `anchor service probe` + | We'll check your running app to ensure TLS works as expected. + - Using org-slug, the only available organization. You can also use `--org org-slug`. + - Using service-name, the only available service. You can also use `--service service-name`. +─── Fetcher[github.com/anchordotdev/cli/api.Realm] ───────────────────────────── + +# Probe Service TLS Setup and Configuration `anchor service probe` + | We'll check your running app to ensure TLS works as expected. + - Using org-slug, the only available organization. You can also use `--org org-slug`. + - Using service-name, the only available service. You can also use `--service service-name`. + * Fetching realms…⠋ +─── Fetcher[github.com/anchordotdev/cli/api.Realm] ───────────────────────────── + +# Probe Service TLS Setup and Configuration `anchor service probe` + | We'll check your running app to ensure TLS works as expected. + - Using org-slug, the only available organization. You can also use `--org org-slug`. + - Using service-name, the only available service. You can also use `--service service-name`. + - Using realm-slug, the only available realm. You can also use `--realm realm-slug`. +─── Checker ──────────────────────────────────────────────────────────────────── + +# Probe Service TLS Setup and Configuration `anchor service probe` + | We'll check your running app to ensure TLS works as expected. + - Using org-slug, the only available organization. You can also use `--org org-slug`. + - Using service-name, the only available service. You can also use `--service service-name`. + - Using realm-slug, the only available realm. You can also use `--realm realm-slug`. + * Checking DNS service.lcl.host…⠋ +─── Checker ──────────────────────────────────────────────────────────────────── + +# Probe Service TLS Setup and Configuration `anchor service probe` + | We'll check your running app to ensure TLS works as expected. + - Using org-slug, the only available organization. You can also use `--org org-slug`. + - Using service-name, the only available service. You can also use `--service service-name`. + - Using realm-slug, the only available realm. You can also use `--realm realm-slug`. + ! Checked DNS service.lcl.host: failed! + ! Error! diff --git a/service/testdata/TestProbeErrors/tcp-failure-connection-failure.golden b/service/testdata/TestProbeErrors/tcp-failure-connection-failure.golden new file mode 100644 index 0000000..7c3f83f --- /dev/null +++ b/service/testdata/TestProbeErrors/tcp-failure-connection-failure.golden @@ -0,0 +1,92 @@ +─── ProbeHeader ──────────────────────────────────────────────────────────────── + +# Probe Service TLS Setup and Configuration `anchor service probe` +─── ProbeHint ────────────────────────────────────────────────────────────────── + +# Probe Service TLS Setup and Configuration `anchor service probe` + | We'll check your running app to ensure TLS works as expected. +─── Client ───────────────────────────────────────────────────────────────────── + +# Probe Service TLS Setup and Configuration `anchor service probe` + | We'll check your running app to ensure TLS works as expected. + * Checking authentication: probing credentials locally…⠋ +─── Client ───────────────────────────────────────────────────────────────────── + +# Probe Service TLS Setup and Configuration `anchor service probe` + | We'll check your running app to ensure TLS works as expected. + * Checking authentication: testing credentials remotely…⠋ +─── Client ───────────────────────────────────────────────────────────────────── + +# Probe Service TLS Setup and Configuration `anchor service probe` + | We'll check your running app to ensure TLS works as expected. +─── Fetcher[github.com/anchordotdev/cli/api.Organization] ────────────────────── + +# Probe Service TLS Setup and Configuration `anchor service probe` + | We'll check your running app to ensure TLS works as expected. + * Fetching organizations…⠋ +─── Fetcher[github.com/anchordotdev/cli/api.Organization] ────────────────────── + +# Probe Service TLS Setup and Configuration `anchor service probe` + | We'll check your running app to ensure TLS works as expected. + - Using org-slug, the only available organization. You can also use `--org org-slug`. +─── Fetcher[github.com/anchordotdev/cli/api.Service] ─────────────────────────── + +# Probe Service TLS Setup and Configuration `anchor service probe` + | We'll check your running app to ensure TLS works as expected. + - Using org-slug, the only available organization. You can also use `--org org-slug`. + * Fetching services…⠋ +─── Fetcher[github.com/anchordotdev/cli/api.Service] ─────────────────────────── + +# Probe Service TLS Setup and Configuration `anchor service probe` + | We'll check your running app to ensure TLS works as expected. + - Using org-slug, the only available organization. You can also use `--org org-slug`. + - Using service-name, the only available service. You can also use `--service service-name`. +─── Fetcher[github.com/anchordotdev/cli/api.Realm] ───────────────────────────── + +# Probe Service TLS Setup and Configuration `anchor service probe` + | We'll check your running app to ensure TLS works as expected. + - Using org-slug, the only available organization. You can also use `--org org-slug`. + - Using service-name, the only available service. You can also use `--service service-name`. + * Fetching realms…⠋ +─── Fetcher[github.com/anchordotdev/cli/api.Realm] ───────────────────────────── + +# Probe Service TLS Setup and Configuration `anchor service probe` + | We'll check your running app to ensure TLS works as expected. + - Using org-slug, the only available organization. You can also use `--org org-slug`. + - Using service-name, the only available service. You can also use `--service service-name`. + - Using realm-slug, the only available realm. You can also use `--realm realm-slug`. +─── Checker ──────────────────────────────────────────────────────────────────── + +# Probe Service TLS Setup and Configuration `anchor service probe` + | We'll check your running app to ensure TLS works as expected. + - Using org-slug, the only available organization. You can also use `--org org-slug`. + - Using service-name, the only available service. You can also use `--service service-name`. + - Using realm-slug, the only available realm. You can also use `--realm realm-slug`. + * Checking DNS service.lcl.host…⠋ +─── Checker ──────────────────────────────────────────────────────────────────── + +# Probe Service TLS Setup and Configuration `anchor service probe` + | We'll check your running app to ensure TLS works as expected. + - Using org-slug, the only available organization. You can also use `--org org-slug`. + - Using service-name, the only available service. You can also use `--service service-name`. + - Using realm-slug, the only available realm. You can also use `--realm realm-slug`. + - Checked DNS service.lcl.host: success! +─── Checker ──────────────────────────────────────────────────────────────────── + +# Probe Service TLS Setup and Configuration `anchor service probe` + | We'll check your running app to ensure TLS works as expected. + - Using org-slug, the only available organization. You can also use `--org org-slug`. + - Using service-name, the only available service. You can also use `--service service-name`. + - Using realm-slug, the only available realm. You can also use `--realm realm-slug`. + - Checked DNS service.lcl.host: success! + * Checking TCP []…⠋ +─── Checker ──────────────────────────────────────────────────────────────────── + +# Probe Service TLS Setup and Configuration `anchor service probe` + | We'll check your running app to ensure TLS works as expected. + - Using org-slug, the only available organization. You can also use `--org org-slug`. + - Using service-name, the only available service. You can also use `--service service-name`. + - Using realm-slug, the only available realm. You can also use `--realm realm-slug`. + - Checked DNS service.lcl.host: success! + ! Checked TCP []: failed! + ! Error! context deadline exceeded diff --git a/service/testdata/TestProbeErrors/tcp-failure-connection-timeout.golden b/service/testdata/TestProbeErrors/tcp-failure-connection-timeout.golden new file mode 100644 index 0000000..7c3f83f --- /dev/null +++ b/service/testdata/TestProbeErrors/tcp-failure-connection-timeout.golden @@ -0,0 +1,92 @@ +─── ProbeHeader ──────────────────────────────────────────────────────────────── + +# Probe Service TLS Setup and Configuration `anchor service probe` +─── ProbeHint ────────────────────────────────────────────────────────────────── + +# Probe Service TLS Setup and Configuration `anchor service probe` + | We'll check your running app to ensure TLS works as expected. +─── Client ───────────────────────────────────────────────────────────────────── + +# Probe Service TLS Setup and Configuration `anchor service probe` + | We'll check your running app to ensure TLS works as expected. + * Checking authentication: probing credentials locally…⠋ +─── Client ───────────────────────────────────────────────────────────────────── + +# Probe Service TLS Setup and Configuration `anchor service probe` + | We'll check your running app to ensure TLS works as expected. + * Checking authentication: testing credentials remotely…⠋ +─── Client ───────────────────────────────────────────────────────────────────── + +# Probe Service TLS Setup and Configuration `anchor service probe` + | We'll check your running app to ensure TLS works as expected. +─── Fetcher[github.com/anchordotdev/cli/api.Organization] ────────────────────── + +# Probe Service TLS Setup and Configuration `anchor service probe` + | We'll check your running app to ensure TLS works as expected. + * Fetching organizations…⠋ +─── Fetcher[github.com/anchordotdev/cli/api.Organization] ────────────────────── + +# Probe Service TLS Setup and Configuration `anchor service probe` + | We'll check your running app to ensure TLS works as expected. + - Using org-slug, the only available organization. You can also use `--org org-slug`. +─── Fetcher[github.com/anchordotdev/cli/api.Service] ─────────────────────────── + +# Probe Service TLS Setup and Configuration `anchor service probe` + | We'll check your running app to ensure TLS works as expected. + - Using org-slug, the only available organization. You can also use `--org org-slug`. + * Fetching services…⠋ +─── Fetcher[github.com/anchordotdev/cli/api.Service] ─────────────────────────── + +# Probe Service TLS Setup and Configuration `anchor service probe` + | We'll check your running app to ensure TLS works as expected. + - Using org-slug, the only available organization. You can also use `--org org-slug`. + - Using service-name, the only available service. You can also use `--service service-name`. +─── Fetcher[github.com/anchordotdev/cli/api.Realm] ───────────────────────────── + +# Probe Service TLS Setup and Configuration `anchor service probe` + | We'll check your running app to ensure TLS works as expected. + - Using org-slug, the only available organization. You can also use `--org org-slug`. + - Using service-name, the only available service. You can also use `--service service-name`. + * Fetching realms…⠋ +─── Fetcher[github.com/anchordotdev/cli/api.Realm] ───────────────────────────── + +# Probe Service TLS Setup and Configuration `anchor service probe` + | We'll check your running app to ensure TLS works as expected. + - Using org-slug, the only available organization. You can also use `--org org-slug`. + - Using service-name, the only available service. You can also use `--service service-name`. + - Using realm-slug, the only available realm. You can also use `--realm realm-slug`. +─── Checker ──────────────────────────────────────────────────────────────────── + +# Probe Service TLS Setup and Configuration `anchor service probe` + | We'll check your running app to ensure TLS works as expected. + - Using org-slug, the only available organization. You can also use `--org org-slug`. + - Using service-name, the only available service. You can also use `--service service-name`. + - Using realm-slug, the only available realm. You can also use `--realm realm-slug`. + * Checking DNS service.lcl.host…⠋ +─── Checker ──────────────────────────────────────────────────────────────────── + +# Probe Service TLS Setup and Configuration `anchor service probe` + | We'll check your running app to ensure TLS works as expected. + - Using org-slug, the only available organization. You can also use `--org org-slug`. + - Using service-name, the only available service. You can also use `--service service-name`. + - Using realm-slug, the only available realm. You can also use `--realm realm-slug`. + - Checked DNS service.lcl.host: success! +─── Checker ──────────────────────────────────────────────────────────────────── + +# Probe Service TLS Setup and Configuration `anchor service probe` + | We'll check your running app to ensure TLS works as expected. + - Using org-slug, the only available organization. You can also use `--org org-slug`. + - Using service-name, the only available service. You can also use `--service service-name`. + - Using realm-slug, the only available realm. You can also use `--realm realm-slug`. + - Checked DNS service.lcl.host: success! + * Checking TCP []…⠋ +─── Checker ──────────────────────────────────────────────────────────────────── + +# Probe Service TLS Setup and Configuration `anchor service probe` + | We'll check your running app to ensure TLS works as expected. + - Using org-slug, the only available organization. You can also use `--org org-slug`. + - Using service-name, the only available service. You can also use `--service service-name`. + - Using realm-slug, the only available realm. You can also use `--realm realm-slug`. + - Checked DNS service.lcl.host: success! + ! Checked TCP []: failed! + ! Error! context deadline exceeded diff --git a/service/testdata/TestProbeErrors/tls-failure-non-tls-server.golden b/service/testdata/TestProbeErrors/tls-failure-non-tls-server.golden new file mode 100644 index 0000000..1550b41 --- /dev/null +++ b/service/testdata/TestProbeErrors/tls-failure-non-tls-server.golden @@ -0,0 +1,112 @@ +─── ProbeHeader ──────────────────────────────────────────────────────────────── + +# Probe Service TLS Setup and Configuration `anchor service probe` +─── ProbeHint ────────────────────────────────────────────────────────────────── + +# Probe Service TLS Setup and Configuration `anchor service probe` + | We'll check your running app to ensure TLS works as expected. +─── Client ───────────────────────────────────────────────────────────────────── + +# Probe Service TLS Setup and Configuration `anchor service probe` + | We'll check your running app to ensure TLS works as expected. + * Checking authentication: probing credentials locally…⠋ +─── Client ───────────────────────────────────────────────────────────────────── + +# Probe Service TLS Setup and Configuration `anchor service probe` + | We'll check your running app to ensure TLS works as expected. + * Checking authentication: testing credentials remotely…⠋ +─── Client ───────────────────────────────────────────────────────────────────── + +# Probe Service TLS Setup and Configuration `anchor service probe` + | We'll check your running app to ensure TLS works as expected. +─── Fetcher[github.com/anchordotdev/cli/api.Organization] ────────────────────── + +# Probe Service TLS Setup and Configuration `anchor service probe` + | We'll check your running app to ensure TLS works as expected. + * Fetching organizations…⠋ +─── Fetcher[github.com/anchordotdev/cli/api.Organization] ────────────────────── + +# Probe Service TLS Setup and Configuration `anchor service probe` + | We'll check your running app to ensure TLS works as expected. + - Using org-slug, the only available organization. You can also use `--org org-slug`. +─── Fetcher[github.com/anchordotdev/cli/api.Service] ─────────────────────────── + +# Probe Service TLS Setup and Configuration `anchor service probe` + | We'll check your running app to ensure TLS works as expected. + - Using org-slug, the only available organization. You can also use `--org org-slug`. + * Fetching services…⠋ +─── Fetcher[github.com/anchordotdev/cli/api.Service] ─────────────────────────── + +# Probe Service TLS Setup and Configuration `anchor service probe` + | We'll check your running app to ensure TLS works as expected. + - Using org-slug, the only available organization. You can also use `--org org-slug`. + - Using service-name, the only available service. You can also use `--service service-name`. +─── Fetcher[github.com/anchordotdev/cli/api.Realm] ───────────────────────────── + +# Probe Service TLS Setup and Configuration `anchor service probe` + | We'll check your running app to ensure TLS works as expected. + - Using org-slug, the only available organization. You can also use `--org org-slug`. + - Using service-name, the only available service. You can also use `--service service-name`. + * Fetching realms…⠋ +─── Fetcher[github.com/anchordotdev/cli/api.Realm] ───────────────────────────── + +# Probe Service TLS Setup and Configuration `anchor service probe` + | We'll check your running app to ensure TLS works as expected. + - Using org-slug, the only available organization. You can also use `--org org-slug`. + - Using service-name, the only available service. You can also use `--service service-name`. + - Using realm-slug, the only available realm. You can also use `--realm realm-slug`. +─── Checker ──────────────────────────────────────────────────────────────────── + +# Probe Service TLS Setup and Configuration `anchor service probe` + | We'll check your running app to ensure TLS works as expected. + - Using org-slug, the only available organization. You can also use `--org org-slug`. + - Using service-name, the only available service. You can also use `--service service-name`. + - Using realm-slug, the only available realm. You can also use `--realm realm-slug`. + * Checking DNS service.lcl.host…⠋ +─── Checker ──────────────────────────────────────────────────────────────────── + +# Probe Service TLS Setup and Configuration `anchor service probe` + | We'll check your running app to ensure TLS works as expected. + - Using org-slug, the only available organization. You can also use `--org org-slug`. + - Using service-name, the only available service. You can also use `--service service-name`. + - Using realm-slug, the only available realm. You can also use `--realm realm-slug`. + - Checked DNS service.lcl.host: success! +─── Checker ──────────────────────────────────────────────────────────────────── + +# Probe Service TLS Setup and Configuration `anchor service probe` + | We'll check your running app to ensure TLS works as expected. + - Using org-slug, the only available organization. You can also use `--org org-slug`. + - Using service-name, the only available service. You can also use `--service service-name`. + - Using realm-slug, the only available realm. You can also use `--realm realm-slug`. + - Checked DNS service.lcl.host: success! + * Checking TCP []…⠋ +─── Checker ──────────────────────────────────────────────────────────────────── + +# Probe Service TLS Setup and Configuration `anchor service probe` + | We'll check your running app to ensure TLS works as expected. + - Using org-slug, the only available organization. You can also use `--org org-slug`. + - Using service-name, the only available service. You can also use `--service service-name`. + - Using realm-slug, the only available realm. You can also use `--realm realm-slug`. + - Checked DNS service.lcl.host: success! + - Checked TCP []: success! +─── Checker ──────────────────────────────────────────────────────────────────── + +# Probe Service TLS Setup and Configuration `anchor service probe` + | We'll check your running app to ensure TLS works as expected. + - Using org-slug, the only available organization. You can also use `--org org-slug`. + - Using service-name, the only available service. You can also use `--service service-name`. + - Using realm-slug, the only available realm. You can also use `--realm realm-slug`. + - Checked DNS service.lcl.host: success! + - Checked TCP []: success! + * Checking TLS service.lcl.host…⠋ +─── Checker ──────────────────────────────────────────────────────────────────── + +# Probe Service TLS Setup and Configuration `anchor service probe` + | We'll check your running app to ensure TLS works as expected. + - Using org-slug, the only available organization. You can also use `--org org-slug`. + - Using service-name, the only available service. You can also use `--service service-name`. + - Using realm-slug, the only available realm. You can also use `--realm realm-slug`. + - Checked DNS service.lcl.host: success! + - Checked TCP []: success! + ! Checked TLS service.lcl.host: failed! + ! Error! tls: first record does not look like a TLS handshake diff --git a/service/testdata/TestProbeErrors/tls-failure-unknown-cert.golden b/service/testdata/TestProbeErrors/tls-failure-unknown-cert.golden new file mode 100644 index 0000000..a581d57 --- /dev/null +++ b/service/testdata/TestProbeErrors/tls-failure-unknown-cert.golden @@ -0,0 +1,112 @@ +─── ProbeHeader ──────────────────────────────────────────────────────────────── + +# Probe Service TLS Setup and Configuration `anchor service probe` +─── ProbeHint ────────────────────────────────────────────────────────────────── + +# Probe Service TLS Setup and Configuration `anchor service probe` + | We'll check your running app to ensure TLS works as expected. +─── Client ───────────────────────────────────────────────────────────────────── + +# Probe Service TLS Setup and Configuration `anchor service probe` + | We'll check your running app to ensure TLS works as expected. + * Checking authentication: probing credentials locally…⠋ +─── Client ───────────────────────────────────────────────────────────────────── + +# Probe Service TLS Setup and Configuration `anchor service probe` + | We'll check your running app to ensure TLS works as expected. + * Checking authentication: testing credentials remotely…⠋ +─── Client ───────────────────────────────────────────────────────────────────── + +# Probe Service TLS Setup and Configuration `anchor service probe` + | We'll check your running app to ensure TLS works as expected. +─── Fetcher[github.com/anchordotdev/cli/api.Organization] ────────────────────── + +# Probe Service TLS Setup and Configuration `anchor service probe` + | We'll check your running app to ensure TLS works as expected. + * Fetching organizations…⠋ +─── Fetcher[github.com/anchordotdev/cli/api.Organization] ────────────────────── + +# Probe Service TLS Setup and Configuration `anchor service probe` + | We'll check your running app to ensure TLS works as expected. + - Using org-slug, the only available organization. You can also use `--org org-slug`. +─── Fetcher[github.com/anchordotdev/cli/api.Service] ─────────────────────────── + +# Probe Service TLS Setup and Configuration `anchor service probe` + | We'll check your running app to ensure TLS works as expected. + - Using org-slug, the only available organization. You can also use `--org org-slug`. + * Fetching services…⠋ +─── Fetcher[github.com/anchordotdev/cli/api.Service] ─────────────────────────── + +# Probe Service TLS Setup and Configuration `anchor service probe` + | We'll check your running app to ensure TLS works as expected. + - Using org-slug, the only available organization. You can also use `--org org-slug`. + - Using service-name, the only available service. You can also use `--service service-name`. +─── Fetcher[github.com/anchordotdev/cli/api.Realm] ───────────────────────────── + +# Probe Service TLS Setup and Configuration `anchor service probe` + | We'll check your running app to ensure TLS works as expected. + - Using org-slug, the only available organization. You can also use `--org org-slug`. + - Using service-name, the only available service. You can also use `--service service-name`. + * Fetching realms…⠋ +─── Fetcher[github.com/anchordotdev/cli/api.Realm] ───────────────────────────── + +# Probe Service TLS Setup and Configuration `anchor service probe` + | We'll check your running app to ensure TLS works as expected. + - Using org-slug, the only available organization. You can also use `--org org-slug`. + - Using service-name, the only available service. You can also use `--service service-name`. + - Using realm-slug, the only available realm. You can also use `--realm realm-slug`. +─── Checker ──────────────────────────────────────────────────────────────────── + +# Probe Service TLS Setup and Configuration `anchor service probe` + | We'll check your running app to ensure TLS works as expected. + - Using org-slug, the only available organization. You can also use `--org org-slug`. + - Using service-name, the only available service. You can also use `--service service-name`. + - Using realm-slug, the only available realm. You can also use `--realm realm-slug`. + * Checking DNS service.lcl.host…⠋ +─── Checker ──────────────────────────────────────────────────────────────────── + +# Probe Service TLS Setup and Configuration `anchor service probe` + | We'll check your running app to ensure TLS works as expected. + - Using org-slug, the only available organization. You can also use `--org org-slug`. + - Using service-name, the only available service. You can also use `--service service-name`. + - Using realm-slug, the only available realm. You can also use `--realm realm-slug`. + - Checked DNS service.lcl.host: success! +─── Checker ──────────────────────────────────────────────────────────────────── + +# Probe Service TLS Setup and Configuration `anchor service probe` + | We'll check your running app to ensure TLS works as expected. + - Using org-slug, the only available organization. You can also use `--org org-slug`. + - Using service-name, the only available service. You can also use `--service service-name`. + - Using realm-slug, the only available realm. You can also use `--realm realm-slug`. + - Checked DNS service.lcl.host: success! + * Checking TCP []…⠋ +─── Checker ──────────────────────────────────────────────────────────────────── + +# Probe Service TLS Setup and Configuration `anchor service probe` + | We'll check your running app to ensure TLS works as expected. + - Using org-slug, the only available organization. You can also use `--org org-slug`. + - Using service-name, the only available service. You can also use `--service service-name`. + - Using realm-slug, the only available realm. You can also use `--realm realm-slug`. + - Checked DNS service.lcl.host: success! + - Checked TCP []: success! +─── Checker ──────────────────────────────────────────────────────────────────── + +# Probe Service TLS Setup and Configuration `anchor service probe` + | We'll check your running app to ensure TLS works as expected. + - Using org-slug, the only available organization. You can also use `--org org-slug`. + - Using service-name, the only available service. You can also use `--service service-name`. + - Using realm-slug, the only available realm. You can also use `--realm realm-slug`. + - Checked DNS service.lcl.host: success! + - Checked TCP []: success! + * Checking TLS service.lcl.host…⠋ +─── Checker ──────────────────────────────────────────────────────────────────── + +# Probe Service TLS Setup and Configuration `anchor service probe` + | We'll check your running app to ensure TLS works as expected. + - Using org-slug, the only available organization. You can also use `--org org-slug`. + - Using service-name, the only available service. You can also use `--service service-name`. + - Using realm-slug, the only available realm. You can also use `--realm realm-slug`. + - Checked DNS service.lcl.host: success! + - Checked TCP []: success! + ! Checked TLS service.lcl.host: failed! + ! Error! tls: failed to verify certificate: x509: certificate is valid for example.com, not service.lcl.host diff --git a/testdata/TestCmdRoot/--help.golden b/testdata/TestCmdRoot/--help.golden index 92d3182..ef1b59b 100644 --- a/testdata/TestCmdRoot/--help.golden +++ b/testdata/TestCmdRoot/--help.golden @@ -12,7 +12,7 @@ Available Commands: help Help about any command lcl Manage lcl.host Local Development Environment trust Manage CA Certificates in your Local Trust Store(s) - version Show version info + version Show Version Info Flags: --api-token string Anchor API personal access token (PAT). diff --git a/testdata/TestCmdRoot/root.golden b/testdata/TestCmdRoot/root.golden index 92d3182..ef1b59b 100644 --- a/testdata/TestCmdRoot/root.golden +++ b/testdata/TestCmdRoot/root.golden @@ -12,7 +12,7 @@ Available Commands: help Help about any command lcl Manage lcl.host Local Development Environment trust Manage CA Certificates in your Local Trust Store(s) - version Show version info + version Show Version Info Flags: --api-token string Anchor API personal access token (PAT). diff --git a/testdata/TestConfigLoadTOMLGolden.golden b/testdata/TestConfigLoadTOMLGolden.golden deleted file mode 100644 index 8fc1c3d..0000000 --- a/testdata/TestConfigLoadTOMLGolden.golden +++ /dev/null @@ -1,2 +0,0 @@ -─── ConfigFileLoaded ─────────────────────────────────────────────────────────── - - Loaded anchor.toml configuration. You can use `--skip-config` to ignore this file. diff --git a/testdata/TestPanic/golden-unix.golden b/testdata/TestPanic/golden-unix.golden index 57c5689..27685ba 100644 --- a/testdata/TestPanic/golden-unix.golden +++ b/testdata/TestPanic/golden-unix.golden @@ -22,4 +22,4 @@ | We are sorry you encountered this error. ! Press Enter to open an issue on Github. ! Warning: Unable to open browser. - ! Open this in a browser to continue: https://github.com/anchordotdev/cli/issues/new?body=%2A%2AAre+there+any+additional+details+you+would+like+to+share%3F%2A%2A%0A%0A---%0A%0A%2A%2ACommand%3A%2A%2A+%60anchor%60%0A%2A%2AExecutable%3A%2A%2A+%60%2Ftmp%2Fgo-build0123456789%2Fb001%2Fexe%2Fanchor%60%0A%2A%2AVersion%3A%2A%2A+%60dev+%28goos%2Fgoarch%29+Commit%3A+none+BuildDate%3A+unknown%60%0A%2A%2AArguments%3A%2A%2A+%60%5B%5D%60%0A%2A%2AFlags%3A%2A%2A+%60%5B%5D%60%0A%2A%2ATimestamp%3A%2A%2A+%602024-01-02T15%3A04%3A05.987654321Z%60%0A%2A%2AStack%3A%2A%2A%0A%60%60%60%0Apanic%28%7B%3Chex%3E%2C+%3Chex%3E%7D%29%0A%09%3Cgoroot%3E%2Fsrc%2Fruntime%2Fpanic.go%3A%3Cline%3E+%2B%3Chex%3E%0Agithub.com%2Fanchordotdev%2Fcli_test.%28%2APanicCommand%29.run%28%3Chex%3E%2C+%7B%3Chex%3E%2C+%3Chex%3E%7D%2C+%3Chex%3E%29%0A%09%3Cpwd%3E%2Fcli_test.go%3A107+%2B%3Chex%3E%0Agithub.com%2Fanchordotdev%2Fcli_test.TestPanic.func1.1%28%29%0A%09%3Cpwd%3E%2Fcli_test.go%3A128+%2B%3Chex%3E%0Agithub.com%2Fanchordotdev%2Fcli%2Fstacktrace.CapturePanic%28%3Chex%3E%29%0A%09%3Cpwd%3E%2Fstacktrace%2Fstacktrace.go%3A40+%2B%3Chex%3E%0Agithub.com%2Fanchordotdev%2Fcli_test.TestPanic.func1%28%3Chex%3E%29%0A%09%3Cpwd%3E%2Fcli_test.go%3A128+%2B%3Chex%3E%0Atesting.tRunner%28%3Chex%3E%2C+%3Chex%3E%29%0A%09%3Cgoroot%3E%2Fsrc%2Ftesting%2Ftesting.go%3A%3Cline%3E+%2B%3Chex%3E%0Acreated+by+testing.%28%2AT%29.Run+in+gouroutine+%3Cint%3E%0A%09%3Cgoroot%3E%2Fsrc%2Ftesting%2Ftesting.go%3A%3Cline%3E+%2B%3Chex%3E%0A%60%60%60%0A%2A%2AStdout%3A%2A%2A%0A%60%60%60%0A++++++++++++++++++++++++++++++++%0A%23+Test+panic+%60anchor+test+panic%60%0A++++%7C+Test+panic+Hint.%0A%60%60%60%0A&title=Error%3A+test+panic. + ! Open this in a browser to continue: https://github.com/anchordotdev/cli/issues/new?body=%2A%2AAre+there+any+additional+details+you+would+like+to+share%3F%2A%2A%0A%0A---%0A%0A%2A%2ACommand%3A%2A%2A+%60anchor%60%0A%2A%2AExecutable%3A%2A%2A+%60%2Ftmp%2Fgo-build0123456789%2Fb001%2Fexe%2Fanchor%60%0A%2A%2AVersion%3A%2A%2A+%60dev+%28goos%2Fgoarch%29+Commit%3A+none+BuildDate%3A+unknown%60%0A%2A%2AArguments%3A%2A%2A+%60%5B%5D%60%0A%2A%2AFlags%3A%2A%2A+%60%5B%5D%60%0A%2A%2ATimestamp%3A%2A%2A+%602024-01-02T15%3A04%3A05.987654321Z%60%0A%2A%2AStack%3A%2A%2A%0A%60%60%60%0Apanic%28%7B%3Chex%3E%2C+%3Chex%3E%7D%29%0A%09%3Cgoroot%3E%2Fsrc%2Fruntime%2Fpanic.go%3A%3Cline%3E+%2B%3Chex%3E%0Agithub.com%2Fanchordotdev%2Fcli_test.%28%2APanicCommand%29.run%28%3Chex%3E%2C+%7B%3Chex%3E%2C+%3Chex%3E%7D%2C+%3Chex%3E%29%0A%09%3Cpwd%3E%2Fcli_test.go%3A106+%2B%3Chex%3E%0Agithub.com%2Fanchordotdev%2Fcli_test.TestPanic.func1.1%28%29%0A%09%3Cpwd%3E%2Fcli_test.go%3A127+%2B%3Chex%3E%0Agithub.com%2Fanchordotdev%2Fcli%2Fstacktrace.CapturePanic%28%3Chex%3E%29%0A%09%3Cpwd%3E%2Fstacktrace%2Fstacktrace.go%3A40+%2B%3Chex%3E%0Agithub.com%2Fanchordotdev%2Fcli_test.TestPanic.func1%28%3Chex%3E%29%0A%09%3Cpwd%3E%2Fcli_test.go%3A127+%2B%3Chex%3E%0Atesting.tRunner%28%3Chex%3E%2C+%3Chex%3E%29%0A%09%3Cgoroot%3E%2Fsrc%2Ftesting%2Ftesting.go%3A%3Cline%3E+%2B%3Chex%3E%0Acreated+by+testing.%28%2AT%29.Run+in+gouroutine+%3Cint%3E%0A%09%3Cgoroot%3E%2Fsrc%2Ftesting%2Ftesting.go%3A%3Cline%3E+%2B%3Chex%3E%0A%60%60%60%0A%2A%2AStdout%3A%2A%2A%0A%60%60%60%0A++++++++++++++++++++++++++++++++%0A%23+Test+panic+%60anchor+test+panic%60%0A++++%7C+Test+panic+Hint.%0A%60%60%60%0A&title=Error%3A+test+panic. diff --git a/testdata/TestPanic/golden-windows.golden b/testdata/TestPanic/golden-windows.golden index 12b529b..8b4272d 100644 --- a/testdata/TestPanic/golden-windows.golden +++ b/testdata/TestPanic/golden-windows.golden @@ -22,4 +22,4 @@ | We are sorry you encountered this error. ! Press Enter to open an issue on Github. ! Warning: Unable to open browser. - ! Open this in a browser to continue: https://github.com/anchordotdev/cli/issues/new?body=%2A%2AAre+there+any+additional+details+you+would+like+to+share%3F%2A%2A%0A%0A---%0A%0A%2A%2ACommand%3A%2A%2A+%60anchor%60%0A%2A%2AExecutable%3A%2A%2A+%60C%3A%5CUsers%5Cusername%5CAppData%5CLocal%5CTemp%5Cgo-build0123456789%2Fb001%2Fexe%2Fanchor.exe%60%0A%2A%2AVersion%3A%2A%2A+%60dev+%28goos%2Fgoarch%29+Commit%3A+none+BuildDate%3A+unknown%60%0A%2A%2AArguments%3A%2A%2A+%60%5B%5D%60%0A%2A%2AFlags%3A%2A%2A+%60%5B%5D%60%0A%2A%2ATimestamp%3A%2A%2A+%602024-01-02T15%3A04%3A05.987654321Z%60%0A%2A%2AStack%3A%2A%2A%0A%60%60%60%0Apanic%28%7B%3Chex%3E%2C+%3Chex%3E%7D%29%0A%09%3Cgoroot%3E%2Fsrc%2Fruntime%2Fpanic.go%3A%3Cline%3E+%2B%3Chex%3E%0Agithub.com%2Fanchordotdev%2Fcli_test.%28%2APanicCommand%29.run%28%3Chex%3E%2C+%7B%3Chex%3E%2C+%3Chex%3E%7D%2C+%3Chex%3E%29%0A%09%3Cpwd%3E%2Fcli_test.go%3A107+%2B%3Chex%3E%0Agithub.com%2Fanchordotdev%2Fcli_test.TestPanic.func1.1%28%29%0A%09%3Cpwd%3E%2Fcli_test.go%3A128+%2B%3Chex%3E%0Agithub.com%2Fanchordotdev%2Fcli%2Fstacktrace.CapturePanic%28%3Chex%3E%29%0A%09%3Cpwd%3E%2Fstacktrace%2Fstacktrace.go%3A40+%2B%3Chex%3E%0Agithub.com%2Fanchordotdev%2Fcli_test.TestPanic.func1%28%3Chex%3E%29%0A%09%3Cpwd%3E%2Fcli_test.go%3A128+%2B%3Chex%3E%0Atesting.tRunner%28%3Chex%3E%2C+%3Chex%3E%29%0A%09%3Cgoroot%3E%2Fsrc%2Ftesting%2Ftesting.go%3A%3Cline%3E+%2B%3Chex%3E%0Acreated+by+testing.%28%2AT%29.Run+in+gouroutine+%3Cint%3E%0A%09%3Cgoroot%3E%2Fsrc%2Ftesting%2Ftesting.go%3A%3Cline%3E+%2B%3Chex%3E%0A%60%60%60%0A%2A%2AStdout%3A%2A%2A%0A%60%60%60%0A++++++++++++++++++++++++++++++++%0A%23+Test+panic+%60anchor+test+panic%60%0A++++%7C+Test+panic+Hint.%0A%60%60%60%0A&title=Error%3A+test+panic. + ! Open this in a browser to continue: https://github.com/anchordotdev/cli/issues/new?body=%2A%2AAre+there+any+additional+details+you+would+like+to+share%3F%2A%2A%0A%0A---%0A%0A%2A%2ACommand%3A%2A%2A+%60anchor%60%0A%2A%2AExecutable%3A%2A%2A+%60C%3A%5CUsers%5Cusername%5CAppData%5CLocal%5CTemp%5Cgo-build0123456789%2Fb001%2Fexe%2Fanchor.exe%60%0A%2A%2AVersion%3A%2A%2A+%60dev+%28goos%2Fgoarch%29+Commit%3A+none+BuildDate%3A+unknown%60%0A%2A%2AArguments%3A%2A%2A+%60%5B%5D%60%0A%2A%2AFlags%3A%2A%2A+%60%5B%5D%60%0A%2A%2ATimestamp%3A%2A%2A+%602024-01-02T15%3A04%3A05.987654321Z%60%0A%2A%2AStack%3A%2A%2A%0A%60%60%60%0Apanic%28%7B%3Chex%3E%2C+%3Chex%3E%7D%29%0A%09%3Cgoroot%3E%2Fsrc%2Fruntime%2Fpanic.go%3A%3Cline%3E+%2B%3Chex%3E%0Agithub.com%2Fanchordotdev%2Fcli_test.%28%2APanicCommand%29.run%28%3Chex%3E%2C+%7B%3Chex%3E%2C+%3Chex%3E%7D%2C+%3Chex%3E%29%0A%09%3Cpwd%3E%2Fcli_test.go%3A106+%2B%3Chex%3E%0Agithub.com%2Fanchordotdev%2Fcli_test.TestPanic.func1.1%28%29%0A%09%3Cpwd%3E%2Fcli_test.go%3A127+%2B%3Chex%3E%0Agithub.com%2Fanchordotdev%2Fcli%2Fstacktrace.CapturePanic%28%3Chex%3E%29%0A%09%3Cpwd%3E%2Fstacktrace%2Fstacktrace.go%3A40+%2B%3Chex%3E%0Agithub.com%2Fanchordotdev%2Fcli_test.TestPanic.func1%28%3Chex%3E%29%0A%09%3Cpwd%3E%2Fcli_test.go%3A127+%2B%3Chex%3E%0Atesting.tRunner%28%3Chex%3E%2C+%3Chex%3E%29%0A%09%3Cgoroot%3E%2Fsrc%2Ftesting%2Ftesting.go%3A%3Cline%3E+%2B%3Chex%3E%0Acreated+by+testing.%28%2AT%29.Run+in+gouroutine+%3Cint%3E%0A%09%3Cgoroot%3E%2Fsrc%2Ftesting%2Ftesting.go%3A%3Cline%3E+%2B%3Chex%3E%0A%60%60%60%0A%2A%2AStdout%3A%2A%2A%0A%60%60%60%0A++++++++++++++++++++++++++++++++%0A%23+Test+panic+%60anchor+test+panic%60%0A++++%7C+Test+panic+Hint.%0A%60%60%60%0A&title=Error%3A+test+panic. diff --git a/truststore/platform.go b/truststore/platform.go index e631ddd..99ccfba 100644 --- a/truststore/platform.go +++ b/truststore/platform.go @@ -105,6 +105,9 @@ func parseCertificate(der []byte) (*x509.Certificate, error) { if strings.HasPrefix(err.Error(), "x509: inner and outer signature algorithm identifiers don't match") { return nil, nil } + if strings.HasPrefix(err.Error(), "x509: negative serial number") { + return nil, nil + } return nil, err } return cert, nil diff --git a/truststore/platform_test.go b/truststore/platform_test.go index d2e298c..5f596b2 100644 --- a/truststore/platform_test.go +++ b/truststore/platform_test.go @@ -38,6 +38,12 @@ func TestParseInvalidCerts(t *testing.T) { data: signatureMismatchData, }, + + { + name: "negative-serial-number", + + data: negativeSerialNumberData, + }, } for _, test := range tests { @@ -73,6 +79,14 @@ hyGljDJFHZVceAO0qHA5gjd0p95Z4l+04NqKPcdV4PjSM3RSX9LiWieizJHezloe gHtW2OUPVe138Ic2OAQNB0e0/0FK6h/B96sYcskvwZF2xnOkjOFJimh5iUPIemtT Oi3a6RdwSBzfJTtO9bSQ+lGdkmJAQ0XB3REJPIcLDz7QIG8cRXX4yFnjaHw0kM12 ZyvlXrsgZkrum/0zNBWAnp/MEeTPJzsl75Fu2C+qO7IRMeirP4/Jf6+SWy3BxXNz +-----END CERTIFICATE-----` + negativeSerialNumberData = `-----BEGIN CERTIFICATE----- +MIIBBTCBraADAgECAgH/MAoGCCqGSM49BAMCMA0xCzAJBgNVBAMTAjopMB4XDTIy +MDQxNDIzNTYwNFoXDTIyMDQxNTAxNTYwNFowDTELMAkGA1UEAxMCOikwWTATBgcq +hkjOPQIBBggqhkjOPQMBBwNCAAQ9ezsIsj+q17K87z/PXE/rfGRN72P/Wyn5d6oo +5M0ZbSatuntMvfKdX79CQxXAxN4oXk3Aov4jVSG12AcDI8ShMAoGCCqGSM49BAMC +A0cAMEQCIBzfBU5eMPT6m5lsR6cXaJILpAaiD9YxOl4v6dT3rzEjAiBHmjnHmAss +RqUAyJKFzqZxOlK2q4j2IYnuj5+LrLGbQA== -----END CERTIFICATE-----` signatureMismatchData = `-----BEGIN CERTIFICATE----- MIICFDCCAX2gAwIBAgIECZcijjALBgkqhkiG9w0BAQUwPDEgMB4GA1UEAwwXY29t @@ -87,6 +101,5 @@ Qss2jW+Av6lRAgMBAAGjJTAjMAsGA1UdDwQEAwIEsDAUBgNVHSUEDTALBgkqhkiG uVbrTzd+Tv8SIfgw8+D4Hf9iLLY33yy6CIMZY2xgfGgBh0suSidoLJt3Pr0fiQGK d5IUuavJmM5HeYXlPfg/WxvtcwaB1DlPxGpe3ZsRi2GPBZpxVS1AdwKUk5GmoH4G J1hlJQKJ8yY= ------END CERTIFICATE----- -` +-----END CERTIFICATE-----` ) diff --git a/ui/driver.go b/ui/driver.go index e0eb7c0..04c5012 100644 --- a/ui/driver.go +++ b/ui/driver.go @@ -40,10 +40,10 @@ type Driver struct { models []tea.Model active tea.Model - goldenMutex sync.Mutex - golden string - - lastView string + goldenMutex sync.Mutex + golden string + lastView string + replacements []string } func NewDriverTest(ctx context.Context) *Driver { @@ -105,6 +105,13 @@ func (d *Driver) Golden() string { return d.golden } +func (d *Driver) Replace(unsafe, safe string) { + d.goldenMutex.Lock() + defer d.goldenMutex.Unlock() + + d.replacements = append(d.replacements, unsafe, safe) +} + type stopMsg struct{} func (d *Driver) Stop() { d.Send(stopMsg{}) } @@ -179,33 +186,49 @@ func (d *Driver) View() string { for _, mdl := range d.models { out += mdl.View() } - normalizedOut := spinnerReplacer.Replace(out) - if out != "" && normalizedOut != d.lastView { - var section string - if mdl, ok := d.active.(interface{ Section() string }); ok { - section = mdl.Section() - } else if kind := reflect.TypeOf(d.active).Kind(); kind == reflect.Interface || kind == reflect.Pointer { - section = reflect.TypeOf(d.active).Elem().Name() - } else { - section = reflect.TypeOf(d.active).Name() - } - - separator := fmt.Sprintf("─── %s ", section) - if separatorRuneCount := utf8.RuneCountInString(separator); separatorRuneCount < 80 { - separator = separator + strings.Repeat("─", 80-utf8.RuneCountInString(separator)) - } else { - runes := []rune(separator) - separator = string(runes[:79]) + "…" - } - d.lastView = normalizedOut - d.goldenMutex.Lock() - defer d.goldenMutex.Unlock() - d.golden += strings.Join([]string{separator, normalizedOut}, "\n") + if out != "" { + d.recordGolden(d.replaced(out)) } + return out } +func (d *Driver) replaced(out string) string { + replaced := spinnerReplacer.Replace(out) + replaced = strings.NewReplacer(d.replacements...).Replace(replaced) + return replaced +} + +func (d *Driver) recordGolden(out string) { + d.goldenMutex.Lock() + defer d.goldenMutex.Unlock() + + if out == d.lastView { + return + } + + var section string + if mdl, ok := d.active.(interface{ Section() string }); ok { + section = mdl.Section() + } else if kind := reflect.TypeOf(d.active).Kind(); kind == reflect.Interface || kind == reflect.Pointer { + section = reflect.TypeOf(d.active).Elem().Name() + } else { + section = reflect.TypeOf(d.active).Name() + } + + separator := fmt.Sprintf("─── %s ", section) + if separatorRuneCount := utf8.RuneCountInString(separator); separatorRuneCount < 80 { + separator = separator + strings.Repeat("─", 80-utf8.RuneCountInString(separator)) + } else { + runes := []rune(separator) + separator = string(runes[:79]) + "…" + } + + d.lastView = out + d.golden += strings.Join([]string{separator, out}, "\n") +} + var ( quitPtr = reflect.ValueOf(tea.Quit).Pointer() exitPtr = reflect.ValueOf(Exit).Pointer() diff --git a/ui/uitest/uitest.go b/ui/uitest/uitest.go index 0f02fbb..0cd28df 100644 --- a/ui/uitest/uitest.go +++ b/ui/uitest/uitest.go @@ -16,7 +16,6 @@ import ( "github.com/charmbracelet/x/exp/teatest" "github.com/muesli/termenv" "github.com/spf13/pflag" - "github.com/stretchr/testify/require" "github.com/anchordotdev/cli" "github.com/anchordotdev/cli/ui" @@ -64,12 +63,6 @@ func (p program) Wait() { // no-op, for TestError and since TestModel doesn't provide a Wait without needing a t.testing } -func TestTUIError(ctx context.Context, t *testing.T, tui cli.UI, msgAndArgs ...interface{}) { - _, errc := testTUI(ctx, t, tui) - err := <-errc - require.Error(t, err, msgAndArgs...) -} - func TestTUIOutput(ctx context.Context, t *testing.T, tui cli.UI) { drv, errc := testTUI(ctx, t, tui) @@ -81,6 +74,10 @@ func TestTUIOutput(ctx context.Context, t *testing.T, tui cli.UI) { } func testTUI(ctx context.Context, t *testing.T, tui cli.UI) (*ui.Driver, chan error) { + if cli.ConfigFromContext(ctx) == nil { + t.Fatal("context is missing a cli.Config") + } + drv := ui.NewDriverTest(ctx) tm := teatest.NewTestModel(t, drv, teatest.WithInitialTermSize(128, 64)) diff --git a/version/command.go b/version/command.go index be8a7ae..60af9a4 100644 --- a/version/command.go +++ b/version/command.go @@ -21,6 +21,8 @@ func (c Command) UI() cli.UI { } func (c Command) runTUI(ctx context.Context, drv *ui.Driver) error { + drv.Activate(ctx, models.VersionHeader) + drv.Activate(ctx, &models.Version{ Arch: cli.Version.Arch, Commit: cli.Version.Commit, diff --git a/version/command_test.go b/version/command_test.go index cbc263c..eede7b7 100644 --- a/version/command_test.go +++ b/version/command_test.go @@ -6,6 +6,7 @@ import ( "runtime" "testing" + "github.com/anchordotdev/cli" "github.com/anchordotdev/cli/cmdtest" _ "github.com/anchordotdev/cli/testflags" "github.com/anchordotdev/cli/ui/uitest" @@ -22,6 +23,8 @@ func TestCommand(t *testing.T) { ctx, cancel := context.WithCancel(context.Background()) defer cancel() + ctx = cli.ContextWithConfig(ctx, cmdtest.Config(ctx)) + cmd := Command{} uitest.TestTUIOutput(ctx, t, cmd.UI()) diff --git a/version/models/upgrade.go b/version/models/upgrade.go new file mode 100644 index 0000000..2ff4da5 --- /dev/null +++ b/version/models/upgrade.go @@ -0,0 +1,48 @@ +package models + +import ( + "fmt" + "strings" + + "github.com/anchordotdev/cli/ui" + tea "github.com/charmbracelet/bubbletea" +) + +var ( + VersionUpgradeHeader = ui.Section{ + Name: "VersionUpgradeHeader", + Model: ui.MessageLines{ + ui.Header(fmt.Sprintf("Check for Upgrade %s", ui.Whisper("`anchor version upgrade`"))), + }, + } + + VersionUpgradeUnavailable = ui.Section{ + Name: "VersionUpgradeUnavailable", + Model: ui.MessageLines{ + ui.StepAlert("Already up to date!"), + ui.StepHint("Your anchor CLI is already up to date, check back soon for updates."), + }, + } +) + +type VersionUpgrade struct { + Command string + InClipboard bool +} + +func (m *VersionUpgrade) Init() tea.Cmd { return nil } + +func (m *VersionUpgrade) Update(msg tea.Msg) (tea.Model, tea.Cmd) { return m, nil } + +func (m *VersionUpgrade) View() string { + var b strings.Builder + + if m.InClipboard { + fmt.Fprintln(&b, ui.StepAlert(fmt.Sprintf("Copied %s to your clipboard.", ui.Announce(m.Command)))) + } + + fmt.Fprintln(&b, ui.StepAlert(fmt.Sprintf("%s `%s` to update to the latest version.", ui.Action("Run"), ui.Emphasize(m.Command)))) + fmt.Fprintln(&b, ui.StepHint(fmt.Sprintf("Not using homebrew? Explore other options here: %s", ui.URL("https://github.com/anchordotdev/cli")))) + + return b.String() +} diff --git a/version/models/version.go b/version/models/version.go index 6c73494..dbc60ef 100644 --- a/version/models/version.go +++ b/version/models/version.go @@ -4,9 +4,19 @@ import ( "fmt" "strings" + "github.com/anchordotdev/cli/ui" tea "github.com/charmbracelet/bubbletea" ) +var ( + VersionHeader = ui.Section{ + Name: "VersionHeader", + Model: ui.MessageLines{ + ui.Header(fmt.Sprintf("Show Version Info %s", ui.Whisper("`anchor version'"))), + }, + } +) + type Version struct { Arch, Commit, Date, OS, Version string } diff --git a/version/testdata/TestCmdVersion/--help.golden b/version/testdata/TestCmdVersion/--help.golden index 21ff016..833567c 100644 --- a/version/testdata/TestCmdVersion/--help.golden +++ b/version/testdata/TestCmdVersion/--help.golden @@ -1,7 +1,11 @@ -Show version info +Show Version Info Usage: anchor version [flags] + anchor version [command] + +Available Commands: + upgrade Check for Upgrade Flags: -h, --help help for version @@ -10,3 +14,5 @@ Global Flags: --api-token string Anchor API personal access token (PAT). --config string Service configuration file. (default "anchor.toml") --skip-config Skip loading configuration file. + +Use "anchor version [command] --help" for more information about a command. diff --git a/version/testdata/TestCommand/golden-darwin_arm64.golden b/version/testdata/TestCommand/golden-darwin_arm64.golden index 5e1fbfc..da81d37 100644 --- a/version/testdata/TestCommand/golden-darwin_arm64.golden +++ b/version/testdata/TestCommand/golden-darwin_arm64.golden @@ -1,2 +1,7 @@ +─── VersionHeader ────────────────────────────────────────────────────────────── + +# Show Version Info `anchor version' ─── Version ──────────────────────────────────────────────────────────────────── + +# Show Version Info `anchor version' dev (darwin/arm64) Commit: none BuildDate: unknown diff --git a/version/testdata/TestCommand/golden-linux_amd64.golden b/version/testdata/TestCommand/golden-linux_amd64.golden index 7f112ef..06b9110 100644 --- a/version/testdata/TestCommand/golden-linux_amd64.golden +++ b/version/testdata/TestCommand/golden-linux_amd64.golden @@ -1,2 +1,7 @@ +─── VersionHeader ────────────────────────────────────────────────────────────── + +# Show Version Info `anchor version' ─── Version ──────────────────────────────────────────────────────────────────── + +# Show Version Info `anchor version' dev (linux/amd64) Commit: none BuildDate: unknown diff --git a/version/testdata/TestCommand/golden-windows_amd64.golden b/version/testdata/TestCommand/golden-windows_amd64.golden index 2fd61ca..986191e 100644 --- a/version/testdata/TestCommand/golden-windows_amd64.golden +++ b/version/testdata/TestCommand/golden-windows_amd64.golden @@ -1,2 +1,7 @@ +─── VersionHeader ────────────────────────────────────────────────────────────── + +# Show Version Info `anchor version' ─── Version ──────────────────────────────────────────────────────────────────── + +# Show Version Info `anchor version' dev (windows/amd64) Commit: none BuildDate: unknown diff --git a/version/testdata/TestUpgrade/upgrade-available-unix.golden b/version/testdata/TestUpgrade/upgrade-available-unix.golden new file mode 100644 index 0000000..f34657f --- /dev/null +++ b/version/testdata/TestUpgrade/upgrade-available-unix.golden @@ -0,0 +1,9 @@ +─── VersionUpgradeHeader ─────────────────────────────────────────────────────── + +# Check for Upgrade `anchor version upgrade` +─── VersionUpgrade ───────────────────────────────────────────────────────────── + +# Check for Upgrade `anchor version upgrade` + ! Copied brew update && brew upgrade anchor to your clipboard. + ! Run `brew update && brew upgrade anchor` to update to the latest version. + | Not using homebrew? Explore other options here: https://github.com/anchordotdev/cli diff --git a/version/testdata/TestUpgrade/upgrade-available-windows.golden b/version/testdata/TestUpgrade/upgrade-available-windows.golden new file mode 100644 index 0000000..d23d89a --- /dev/null +++ b/version/testdata/TestUpgrade/upgrade-available-windows.golden @@ -0,0 +1,9 @@ +─── VersionUpgradeHeader ─────────────────────────────────────────────────────── + +# Check for Upgrade `anchor version upgrade` +─── VersionUpgrade ───────────────────────────────────────────────────────────── + +# Check for Upgrade `anchor version upgrade` + ! Copied winget update Anchor.cli to your clipboard. + ! Run `winget update Anchor.cli` to update to the latest version. + | Not using homebrew? Explore other options here: https://github.com/anchordotdev/cli diff --git a/version/testdata/TestUpgrade/upgrade-unavailable.golden b/version/testdata/TestUpgrade/upgrade-unavailable.golden new file mode 100644 index 0000000..e6e42e5 --- /dev/null +++ b/version/testdata/TestUpgrade/upgrade-unavailable.golden @@ -0,0 +1,8 @@ +─── VersionUpgradeHeader ─────────────────────────────────────────────────────── + +# Check for Upgrade `anchor version upgrade` +─── VersionUpgradeUnavailable ────────────────────────────────────────────────── + +# Check for Upgrade `anchor version upgrade` + ! Already up to date! + | Your anchor CLI is already up to date, check back soon for updates. diff --git a/version/upgrade.go b/version/upgrade.go new file mode 100644 index 0000000..6002510 --- /dev/null +++ b/version/upgrade.go @@ -0,0 +1,58 @@ +package version + +import ( + "context" + + "github.com/spf13/cobra" + + "github.com/anchordotdev/cli" + "github.com/anchordotdev/cli/clipboard" + "github.com/anchordotdev/cli/ui" + "github.com/anchordotdev/cli/version/models" +) + +var CmdVersionUpgrade = cli.NewCmd[Upgrade](CmdVersion, "upgrade", func(cmd *cobra.Command) {}) + +type Upgrade struct { + Clipboard cli.Clipboard +} + +func (c Upgrade) UI() cli.UI { + return cli.UI{ + RunTUI: c.runTUI, + } +} + +func (c Upgrade) runTUI(ctx context.Context, drv *ui.Driver) error { + cli.SkipReleaseCheck = true + + drv.Activate(ctx, models.VersionUpgradeHeader) + + if c.Clipboard == nil { + c.Clipboard = clipboard.System + } + + command := "brew update && brew upgrade anchor" + if isWindowsRuntime(cli.ConfigFromContext(ctx)) { + command = "winget update Anchor.cli" + } + + isUpgradeable, err := cli.IsUpgradeable(ctx) + if err != nil { + return err + } + + clipboardErr := c.Clipboard.WriteAll(command) + + if isUpgradeable { + drv.Activate(ctx, &models.VersionUpgrade{ + InClipboard: (clipboardErr == nil), + Command: command, + }) + + return nil + } + + drv.Activate(ctx, &models.VersionUpgradeUnavailable) + return nil +} diff --git a/version/upgrade_test.go b/version/upgrade_test.go new file mode 100644 index 0000000..a6a0b25 --- /dev/null +++ b/version/upgrade_test.go @@ -0,0 +1,58 @@ +package version + +import ( + "context" + "fmt" + "testing" + + "github.com/anchordotdev/cli" + "github.com/anchordotdev/cli/clipboard" + "github.com/anchordotdev/cli/cmdtest" + "github.com/anchordotdev/cli/ui/uitest" +) + +func TestUpgrade(t *testing.T) { + ctx, cancel := context.WithCancel(context.Background()) + defer cancel() + + cfg := cmdtest.Config(ctx) + ctx = cli.ContextWithConfig(ctx, cfg) + + t.Run(fmt.Sprintf("upgrade-available-%s", uitest.TestTagOS()), func(t *testing.T) { + tagName := "vupgrade" + cli.LatestRelease = &cli.Release{ + TagName: &tagName, + } + cmd := Upgrade{ + Clipboard: new(clipboard.Mock), + } + + uitest.TestTUIOutput(ctx, t, cmd.UI()) + + command, err := cmd.Clipboard.ReadAll() + if err != nil { + t.Fatal(err) + } + + var want string + if cfg.GOOS() == "windows" { + want = "winget update Anchor.cli" + } else { + want = "brew update && brew upgrade anchor" + } + if got := command; want != got { + t.Errorf("Want command:\n\n%q,\n\nGot:\n\n%q\n\n", want, got) + } + }) + + t.Run("upgrade-unavailable", func(t *testing.T) { + tagName := "vdev" + cli.LatestRelease = &cli.Release{ + TagName: &tagName, + } + cmd := Upgrade{} + + uitest.TestTUIOutput(ctx, t, cmd.UI()) + }) + +} diff --git a/version/version.go b/version/version.go index 066b3ed..62b2e81 100644 --- a/version/version.go +++ b/version/version.go @@ -2,11 +2,8 @@ package version import ( "fmt" - "time" "github.com/Masterminds/semver" - "github.com/atotto/clipboard" - "github.com/google/go-github/v54/github" "github.com/spf13/cobra" "github.com/anchordotdev/cli" @@ -14,34 +11,28 @@ import ( ) func ReleaseCheck(cmd *cobra.Command, args []string) error { - if cli.IsDevVersion() { + if cli.SkipReleaseCheck || cli.IsDevVersion() { return nil } ctx := cmd.Context() - release, _, err := github.NewClient(nil).Repositories.GetLatestRelease(ctx, "anchordotdev", "cli") + isFresh, err := cli.IsFreshLatestRelease(ctx) if err != nil { - return nil + return err } - if publishedAt := release.PublishedAt.GetTime(); publishedAt != nil && time.Since(*publishedAt).Hours() < 24 { + if isFresh { return nil } - if release.TagName == nil || *release.TagName != cli.ReleaseTagName() { - fmt.Println(ui.Header("New CLI Release")) - fmt.Println(ui.StepHint("A new release of the anchor CLI is available.")) - - command := "brew update && brew upgrade anchor" - if isWindowsRuntime(cli.ConfigFromCmd(cmd)) { - command = "winget update Anchor.cli" - } + isUpgradeable, err := cli.IsUpgradeable(ctx) + if err != nil { + return err + } - if err := clipboard.WriteAll(command); err == nil { - fmt.Println(ui.StepAlert(fmt.Sprintf("Copied %s to your clipboard.", ui.Announce(command)))) - } - fmt.Println(ui.StepAlert(fmt.Sprintf("%s `%s` to update to the latest version.", ui.Action("Run"), ui.Emphasize(command)))) - fmt.Println(ui.StepHint(fmt.Sprintf("Not using homebrew? Explore other options here: %s", ui.URL("https://github.com/anchordotdev/cli")))) + if isUpgradeable { + fmt.Println(ui.Header("New CLI Release Available")) + fmt.Println(ui.StepAlert("Run `anchor version upgrade` to upgrade.")) } return nil }