diff --git a/errors/errors.go b/errors/errors.go index ed2dcf42cf..efa4a79d73 100644 --- a/errors/errors.go +++ b/errors/errors.go @@ -80,7 +80,7 @@ var ( ErrUnauthorizedAccess = errors.New("auth: unauthorized access. check credentials") ErrCannotResetConfigKey = errors.New("cli: cannot reset given config key") ErrConfigNotFound = errors.New("cli: config with the given name does not exist") - ErrNoURLProvided = errors.New("cli: no URL provided in argument or via config") + ErrNoURLProvided = errors.New("cli: no URL provided by flag or via config") ErrIllegalConfigKey = errors.New("cli: given config key is not allowed") ErrScanNotSupported = errors.New("search: scanning of image media type not supported") ErrCLITimeout = errors.New("cli: Query timed out while waiting for results") @@ -157,6 +157,7 @@ var ( ErrGQLEndpointNotFound = errors.New("cli: the server doesn't have a gql endpoint") ErrGQLQueryNotSupported = errors.New("cli: query is not supported or has different arguments") ErrBadHTTPStatusCode = errors.New("cli: the response doesn't contain the expected status code") + ErrFormatNotSupported = errors.New("cli: the given output format is not supported") ErrFileAlreadyCancelled = errors.New("storageDriver: file already cancelled") ErrFileAlreadyClosed = errors.New("storageDriver: file already closed") ErrFileAlreadyCommitted = errors.New("storageDriver: file already committed") diff --git a/pkg/cli/client/cli.go b/pkg/cli/client/cli.go index 1c0171a7aa..fb57412bba 100644 --- a/pkg/cli/client/cli.go +++ b/pkg/cli/client/cli.go @@ -11,4 +11,5 @@ func enableCli(rootCmd *cobra.Command) { rootCmd.AddCommand(NewCVECommand(NewSearchService())) rootCmd.AddCommand(NewRepoCommand(NewSearchService())) rootCmd.AddCommand(NewSearchCommand(NewSearchService())) + rootCmd.AddCommand(NewServerInfoCommand()) } diff --git a/pkg/cli/client/server_info_cmd.go b/pkg/cli/client/server_info_cmd.go new file mode 100644 index 0000000000..3f6c14ca10 --- /dev/null +++ b/pkg/cli/client/server_info_cmd.go @@ -0,0 +1,111 @@ +//go:build search +// +build search + +package client + +import ( + "context" + "encoding/json" + "fmt" + "strings" + + "github.com/spf13/cobra" + "gopkg.in/yaml.v2" + + zerr "zotregistry.io/zot/errors" + "zotregistry.io/zot/pkg/api/constants" + "zotregistry.io/zot/pkg/cli/cmdflags" +) + +func NewServerInfoCommand() *cobra.Command { + serverInfoCmd := &cobra.Command{ + Use: "server-info", + Short: "Information about the server configuration and build information", + Long: `Information about the server configuration and build information`, + Args: cobra.NoArgs, + RunE: func(cmd *cobra.Command, args []string) error { + searchConfig, err := GetSearchConfigFromFlags(cmd, NewSearchService()) + if err != nil { + return err + } + + return GetServerInfo(searchConfig) + }, + } + + serverInfoCmd.Flags().String(cmdflags.OutputFormatFlag, "text", "Specify the output format [text|json|yaml]") + + return serverInfoCmd +} + +func GetServerInfo(config searchConfig) error { + username, password := getUsernameAndPassword(config.user) + ctx := context.Background() + + mgmtEndpoint, err := combineServerAndEndpointURL(config.servURL, fmt.Sprintf("%s%s", + constants.RoutePrefix, constants.ExtMgmt)) + if err != nil { + return err + } + + serverInfo := ServerInfo{} + + _, err = makeGETRequest(ctx, mgmtEndpoint, username, password, config.verifyTLS, config.debug, + &serverInfo, config.resultWriter) + if err != nil { + return err + } + + outputResult, err := serverInfo.ToStringFormat(config.outputFormat) + if err != nil { + return err + } + + fmt.Fprintln(config.resultWriter, outputResult) + + return nil +} + +type ServerInfo struct { + DistSpecVersion string `json:"distSpecVersion" mapstructure:"distSpecVersion"` + Commit string `json:"commit" mapstructure:"commit"` + BinaryType string `json:"binaryType" mapstructure:"binaryType"` + ReleaseTag string `json:"releaseTag" mapstructure:"releaseTag"` +} + +func (si *ServerInfo) ToStringFormat(format string) (string, error) { + switch format { + case "text", "": + return si.ToText() + case "json": + return si.ToJSON() + case "yaml", "yml": + return si.ToYAML() + default: + return "", zerr.ErrFormatNotSupported + } +} + +func (si *ServerInfo) ToText() (string, error) { + flagsList := strings.Split(strings.Trim(si.BinaryType, "-"), "-") + flags := strings.Join(flagsList, ", ") + + return fmt.Sprintf("Server Version: %s\n"+ + "Dist Spec Version: %s\n"+ + "Built with: %s", + si.ReleaseTag, si.DistSpecVersion, flags, + ), + nil +} + +func (si *ServerInfo) ToJSON() (string, error) { + blob, err := json.MarshalIndent(*si, "", " ") + + return string(blob), err +} + +func (si *ServerInfo) ToYAML() (string, error) { + body, err := yaml.Marshal(*si) + + return string(body), err +} diff --git a/pkg/cli/client/server_info_cmd_test.go b/pkg/cli/client/server_info_cmd_test.go new file mode 100644 index 0000000000..8600f0d0ea --- /dev/null +++ b/pkg/cli/client/server_info_cmd_test.go @@ -0,0 +1,99 @@ +//go:build search +// +build search + +package client //nolint:testpackage + +import ( + "bytes" + "fmt" + "os" + "regexp" + "strings" + "testing" + + . "github.com/smartystreets/goconvey/convey" + + "zotregistry.io/zot/pkg/api" + "zotregistry.io/zot/pkg/api/config" + extconf "zotregistry.io/zot/pkg/extensions/config" + test "zotregistry.io/zot/pkg/test/common" +) + +func TestServerInfoCommand(t *testing.T) { + Convey("ServerInfoCommand", t, func() { + port := test.GetFreePort() + baseURL := test.GetBaseURL(port) + conf := config.New() + conf.HTTP.Port = port + conf.Storage.GC = false + defaultVal := true + conf.Extensions = &extconf.ExtensionConfig{ + Search: &extconf.SearchConfig{BaseConfig: extconf.BaseConfig{Enable: &defaultVal}}, + } + + ctlr := api.NewController(conf) + ctlr.Config.Storage.RootDirectory = t.TempDir() + cm := test.NewControllerManager(ctlr) + + cm.StartAndWait(conf.HTTP.Port) + defer cm.StopServer() + + configPath := makeConfigFile(fmt.Sprintf(`{"configs":[{"_name":"server-info-test","url":"%s","showspinner":false}]}`, + baseURL)) + defer os.Remove(configPath) + + args := []string{"server-info", "--config", "server-info-test"} + cmd := NewCliRootCmd() + buff := bytes.NewBufferString("") + cmd.SetOut(buff) + cmd.SetErr(buff) + cmd.SetArgs(args) + err := cmd.Execute() + So(err, ShouldBeNil) + space := regexp.MustCompile(`\s+`) + str := space.ReplaceAllString(buff.String(), " ") + actual := strings.TrimSpace(str) + So(actual, ShouldContainSubstring, config.ReleaseTag) + So(actual, ShouldContainSubstring, config.BinaryType) + + // JSON + args = []string{"server-info", "--config", "server-info-test", "--format", "json"} + cmd = NewCliRootCmd() + buff = bytes.NewBufferString("") + cmd.SetOut(buff) + cmd.SetErr(buff) + cmd.SetArgs(args) + err = cmd.Execute() + So(err, ShouldBeNil) + space = regexp.MustCompile(`\s+`) + str = space.ReplaceAllString(buff.String(), " ") + actual = strings.TrimSpace(str) + So(actual, ShouldContainSubstring, config.ReleaseTag) + So(actual, ShouldContainSubstring, config.BinaryType) + + // YAML + args = []string{"server-info", "--config", "server-info-test", "--format", "yaml"} + cmd = NewCliRootCmd() + buff = bytes.NewBufferString("") + cmd.SetOut(buff) + cmd.SetErr(buff) + cmd.SetArgs(args) + err = cmd.Execute() + So(err, ShouldBeNil) + space = regexp.MustCompile(`\s+`) + str = space.ReplaceAllString(buff.String(), " ") + actual = strings.TrimSpace(str) + So(actual, ShouldContainSubstring, config.ReleaseTag) + So(actual, ShouldContainSubstring, config.BinaryType) + + // bad type + args = []string{"server-info", "--config", "server-info-test", "--format", "badType"} + cmd = NewCliRootCmd() + buff = bytes.NewBufferString("") + cmd.SetOut(buff) + cmd.SetErr(buff) + cmd.SetArgs(args) + err = cmd.Execute() + So(err, ShouldNotBeNil) + }) +} diff --git a/pkg/cli/server/extensions_test.go b/pkg/cli/server/extensions_test.go index c27c9948dc..5f41f4558e 100644 --- a/pkg/cli/server/extensions_test.go +++ b/pkg/cli/server/extensions_test.go @@ -1178,7 +1178,7 @@ func TestServeMgmtExtension(t *testing.T) { So(found, ShouldBeTrue) }) - Convey("Mgmt disabled - search unconfigured", t, func(c C) { + Convey("Mgmt disabled - Search unconfigured", t, func(c C) { content := `{ "storage": { "rootDirectory": "%s" @@ -1192,9 +1192,6 @@ func TestServeMgmtExtension(t *testing.T) { "output": "%s" }, "extensions": { - "search": { - "enable": false - } } }` diff --git a/pkg/extensions/extension_mgmt.go b/pkg/extensions/extension_mgmt.go index e86cdc2144..88b60f492f 100644 --- a/pkg/extensions/extension_mgmt.go +++ b/pkg/extensions/extension_mgmt.go @@ -43,7 +43,9 @@ type Auth struct { type StrippedConfig struct { DistSpecVersion string `json:"distSpecVersion" mapstructure:"distSpecVersion"` - BinaryType string `json:"binaryType" mapstructure:"binaryType"` + Commit string `json:"commit" mapstructure:"commit"` + ReleaseTag string `json:"releaseTag" mapstructure:"releaseTag"` + BinaryType string `json:"binaryType" mapstructure:"binaryType"` HTTP struct { Auth *Auth `json:"auth,omitempty" mapstructure:"auth"` } `json:"http" mapstructure:"http"` diff --git a/swagger/docs.go b/swagger/docs.go index fbc2067726..5ccb8a2b35 100644 --- a/swagger/docs.go +++ b/swagger/docs.go @@ -1391,6 +1391,9 @@ const docTemplate = `{ "binaryType": { "type": "string" }, + "commit": { + "type": "string" + }, "distSpecVersion": { "type": "string" }, @@ -1401,6 +1404,9 @@ const docTemplate = `{ "$ref": "#/definitions/extensions.Auth" } } + }, + "releaseTag": { + "type": "string" } } }, diff --git a/swagger/swagger.json b/swagger/swagger.json index 50e77cbc27..1b2b28f107 100644 --- a/swagger/swagger.json +++ b/swagger/swagger.json @@ -1382,6 +1382,9 @@ "binaryType": { "type": "string" }, + "commit": { + "type": "string" + }, "distSpecVersion": { "type": "string" }, @@ -1392,6 +1395,9 @@ "$ref": "#/definitions/extensions.Auth" } } + }, + "releaseTag": { + "type": "string" } } }, diff --git a/swagger/swagger.yaml b/swagger/swagger.yaml index c0d0a465f1..2139546ff5 100644 --- a/swagger/swagger.yaml +++ b/swagger/swagger.yaml @@ -154,6 +154,8 @@ definitions: properties: binaryType: type: string + commit: + type: string distSpecVersion: type: string http: @@ -161,6 +163,8 @@ definitions: auth: $ref: '#/definitions/extensions.Auth' type: object + releaseTag: + type: string type: object github_com_opencontainers_image-spec_specs-go_v1.Descriptor: properties: