Skip to content

Commit

Permalink
feat(cli): add command to interogate the server version and other det…
Browse files Browse the repository at this point in the history
…ails

Signed-off-by: Laurentiu Niculae <[email protected]>
  • Loading branch information
laurentiuNiculae committed Nov 17, 2023
1 parent 8e7b2d2 commit f52aef9
Show file tree
Hide file tree
Showing 10 changed files with 336 additions and 7 deletions.
5 changes: 4 additions & 1 deletion errors/errors.go
Original file line number Diff line number Diff line change
Expand Up @@ -78,9 +78,10 @@ var (
ErrInvalidURL = errors.New("cli: invalid URL format")
ErrExtensionNotEnabled = errors.New("cli: functionality is not built/configured in the current server")
ErrUnauthorizedAccess = errors.New("auth: unauthorized access. check credentials")
ErrPageNotFound = errors.New("page not found")
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")
Expand Down Expand Up @@ -157,6 +158,8 @@ 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")
ErrAPINotSupported = errors.New("registry at the given address doesn't implement the correct API")
ErrFileAlreadyCancelled = errors.New("storageDriver: file already cancelled")
ErrFileAlreadyClosed = errors.New("storageDriver: file already closed")
ErrFileAlreadyCommitted = errors.New("storageDriver: file already committed")
Expand Down
1 change: 1 addition & 0 deletions pkg/cli/client/cli.go
Original file line number Diff line number Diff line change
Expand Up @@ -11,4 +11,5 @@ func enableCli(rootCmd *cobra.Command) {
rootCmd.AddCommand(NewCVECommand(NewSearchService()))
rootCmd.AddCommand(NewRepoCommand(NewSearchService()))
rootCmd.AddCommand(NewSearchCommand(NewSearchService()))
rootCmd.AddCommand(NewServerInfoCommand())
}
5 changes: 4 additions & 1 deletion pkg/cli/client/client.go
Original file line number Diff line number Diff line change
Expand Up @@ -119,7 +119,10 @@ func doHTTPRequest(req *http.Request, verifyTLS bool, debug bool,
defer resp.Body.Close()

if resp.StatusCode != http.StatusOK {
if resp.StatusCode == http.StatusUnauthorized {
switch resp.StatusCode {
case http.StatusNotFound:
return nil, zerr.ErrPageNotFound
case http.StatusUnauthorized:
return nil, zerr.ErrUnauthorizedAccess
}

Expand Down
182 changes: 182 additions & 0 deletions pkg/cli/client/server_info_cmd.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,182 @@
//go:build search
// +build search

package client

import (
"context"
"encoding/json"
"errors"
"fmt"
"strings"

"github.com/spf13/cobra"
"gopkg.in/yaml.v2"

zerr "zotregistry.io/zot/errors"
"zotregistry.io/zot/pkg/api/constants"
)

func NewServerInfoCommand() *cobra.Command {
serverInfoCmd := &cobra.Command{
Use: "status",
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.PersistentFlags().String(URLFlag, "",
"Specify zot server URL if config-name is not mentioned")
serverInfoCmd.PersistentFlags().StringP(ConfigFlag, "c", "",
"Specify the registry configuration to use for connection")
serverInfoCmd.PersistentFlags().StringP(UserFlag, "u", "",
`User Credentials of zot server in "username:password" format`)
serverInfoCmd.Flags().StringP(OutputFormatFlag, "f", "text", "Specify the output format [text|json|yaml]")

return serverInfoCmd
}

const (
StatusOnline = "online"
StatusOffline = "offline"
StatusUnknown = "unknown"
)

func GetServerInfo(config SearchConfig) error {
ctx := context.Background()
username, password := getUsernameAndPassword(config.User)

checkAPISupportEndpoint, err := combineServerAndEndpointURL(config.ServURL, constants.RoutePrefix)
if err != nil {
return err
}

_, err = makeGETRequest(ctx, checkAPISupportEndpoint, username, password, config.VerifyTLS, config.Debug,
nil, config.ResultWriter)
if err != nil {
serverInfo := ServerInfo{}

switch {
case errors.Is(err, zerr.ErrUnauthorizedAccess):
serverInfo.Status = StatusUnknown
serverInfo.ErrorMsg = "unauthorised access. given credentials are incorrect"
case errors.Is(err, zerr.ErrBadHTTPStatusCode), errors.Is(err, zerr.ErrPageNotFound):
serverInfo.Status = StatusOffline
serverInfo.ErrorMsg = fmt.Sprintf("%s: request at %s failed", zerr.ErrAPINotSupported.Error(),
checkAPISupportEndpoint)
default:
serverInfo.Status = StatusOffline
serverInfo.ErrorMsg = err.Error()
}

return PrintServerInfo(serverInfo, config)
}

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)

switch {
case err == nil:
serverInfo.Status = StatusOnline
case errors.Is(err, zerr.ErrPageNotFound):
serverInfo.Status = StatusOnline
serverInfo.ErrorMsg = "MGMT endpoint is not enabled"
case errors.Is(err, zerr.ErrUnauthorizedAccess):
serverInfo.Status = StatusOnline
serverInfo.ErrorMsg = "credentials provided have been rejected"
case errors.Is(err, zerr.ErrBadHTTPStatusCode):
serverInfo.Status = StatusOnline
serverInfo.ErrorMsg = err.Error()
default:
serverInfo.Status = StatusOffline
serverInfo.ErrorMsg = err.Error()
}

return PrintServerInfo(serverInfo, config)
}

func PrintServerInfo(serverInfo ServerInfo, config SearchConfig) error {
outputResult, err := serverInfo.ToStringFormat(config.OutputFormat)
if err != nil {
return err
}

fmt.Fprintln(config.ResultWriter, outputResult)

return nil
}

type ServerInfo struct {
Status string `json:"status,omitempty" mapstructure:"status"`
ErrorMsg string `json:"error,omitempty" mapstructure:"error"`
DistSpecVersion string `json:"distSpecVersion,omitempty" mapstructure:"distSpecVersion"`
Commit string `json:"commit,omitempty" mapstructure:"commit"`
BinaryType string `json:"binaryType,omitempty" mapstructure:"binaryType"`
ReleaseTag string `json:"releaseTag,omitempty" 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, ", ")

var output string

if si.ErrorMsg != "" {
serverStatus := fmt.Sprintf("Server Status: %s\n"+
"Error: %s", si.Status, si.ErrorMsg)

output = serverStatus
} else {
serverStatus := fmt.Sprintf("Server Status: %s", si.Status)
serverInfo := fmt.Sprintf("Server Version: %s\n"+
"Dist Spec Version: %s\n"+
"Built with: %s",
si.ReleaseTag, si.DistSpecVersion, flags,
)

output = serverStatus + "\n" + serverInfo + "\n"
}

return output, 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
}
124 changes: 124 additions & 0 deletions pkg/cli/client/server_info_cmd_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,124 @@
//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":"status-test","url":"%s","showspinner":false}]}`,
baseURL))
defer os.Remove(configPath)

args := []string{"status", "--config", "status-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{"status", "--config", "status-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{"status", "--config", "status-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{"status", "--config", "status-test", "--format", "badType"}
cmd = NewCliRootCmd()
buff = bytes.NewBufferString("")
cmd.SetOut(buff)
cmd.SetErr(buff)
cmd.SetArgs(args)
err = cmd.Execute()
So(err, ShouldNotBeNil)
})
}

func TestServerInfoCommandErrors(t *testing.T) {
Convey("ServerInfoCommand", t, func() {
args := []string{"status"}
cmd := NewCliRootCmd()
buff := bytes.NewBufferString("")
cmd.SetOut(buff)
cmd.SetErr(buff)
cmd.SetArgs(args)
err := cmd.Execute()
So(err, ShouldNotBeNil)

// invalid URL
err = GetServerInfo(SearchConfig{
ServURL: "a: ds",
})
So(err, ShouldNotBeNil)

// fail Get request
err = GetServerInfo(SearchConfig{
ServURL: "http://127.0.0.1:8000",
})
So(err, ShouldNotBeNil)
})
}
5 changes: 1 addition & 4 deletions pkg/cli/server/extensions_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -1179,7 +1179,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"
Expand All @@ -1193,9 +1193,6 @@ func TestServeMgmtExtension(t *testing.T) {
"output": "%s"
},
"extensions": {
"search": {
"enable": false
}
}
}`

Expand Down
5 changes: 4 additions & 1 deletion pkg/extensions/extension_mgmt.go
Original file line number Diff line number Diff line change
Expand Up @@ -43,8 +43,11 @@ type Auth struct {

type StrippedConfig struct {
DistSpecVersion string `json:"distSpecVersion" mapstructure:"distSpecVersion"`
Commit string `json:"commit" mapstructure:"commit"`
ReleaseTag string `json:"releaseTag" mapstructure:"releaseTag"`
BinaryType string `json:"binaryType" mapstructure:"binaryType"`
HTTP struct {

HTTP struct {
Auth *Auth `json:"auth,omitempty" mapstructure:"auth"`
} `json:"http" mapstructure:"http"`
}
Expand Down
6 changes: 6 additions & 0 deletions swagger/docs.go

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Loading

0 comments on commit f52aef9

Please sign in to comment.