Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Cli server version #1709

Merged
merged 1 commit into from
Nov 22, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 4 additions & 1 deletion errors/errors.go
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,7 @@
var internalErr *Error
details := make(map[string]string)

if errors.As(err, &internalErr) {

Check failure on line 41 in errors/errors.go

View workflow job for this annotation

GitHub Actions / coverage

condition "errors.As(err, &internalErr)" was never evaluated

Check failure on line 41 in errors/errors.go

View workflow job for this annotation

GitHub Actions / coverage

condition "errors.As(err, &internalErr)" was never evaluated
details = internalErr.GetDetails()
}

Expand Down Expand Up @@ -78,9 +78,10 @@
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")
ErrURLNotFound = errors.New("url 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 @@
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(NewServerStatusCommand())
}
13 changes: 10 additions & 3 deletions pkg/cli/client/client.go
Original file line number Diff line number Diff line change
Expand Up @@ -119,13 +119,20 @@ func doHTTPRequest(req *http.Request, verifyTLS bool, debug bool,
defer resp.Body.Close()

if resp.StatusCode != http.StatusOK {
if resp.StatusCode == http.StatusUnauthorized {
return nil, zerr.ErrUnauthorizedAccess
var err error

switch resp.StatusCode {
case http.StatusNotFound:
err = zerr.ErrURLNotFound
case http.StatusUnauthorized:
err = zerr.ErrUnauthorizedAccess
default:
err = zerr.ErrBadHTTPStatusCode
}

bodyBytes, _ := io.ReadAll(resp.Body)

return nil, fmt.Errorf("%w: Expected: %d, Got: %d, Body: '%s'", zerr.ErrBadHTTPStatusCode, http.StatusOK,
return nil, fmt.Errorf("%w: Expected: %d, Got: %d, Body: '%s'", err, http.StatusOK,
resp.StatusCode, string(bodyBytes))
}

Expand Down
191 changes: 191 additions & 0 deletions pkg/cli/client/server_info_cmd.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,191 @@
//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"
)

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

func NewServerStatusCommand() *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 GetServerStatus(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
}

func GetServerStatus(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 = fmt.Sprintf("unauthorised access, %s", getCredentialsSuggestion(username))
case errors.Is(err, zerr.ErrBadHTTPStatusCode), errors.Is(err, zerr.ErrURLNotFound):
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
}

Check warning on line 87 in pkg/cli/client/server_info_cmd.go

View check run for this annotation

Codecov / codecov/patch

pkg/cli/client/server_info_cmd.go#L86-L87

Added lines #L86 - L87 were not covered by tests

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.ErrURLNotFound):
serverInfo.Status = StatusOnline
laurentiuNiculae marked this conversation as resolved.
Show resolved Hide resolved
serverInfo.ErrorMsg = fmt.Sprintf("%s%s endpoint is not available", constants.RoutePrefix, constants.ExtMgmt)
case errors.Is(err, zerr.ErrUnauthorizedAccess):
serverInfo.Status = StatusOnline
serverInfo.ErrorMsg = fmt.Sprintf("unauthorised access, %s", getCredentialsSuggestion(username))
case errors.Is(err, zerr.ErrBadHTTPStatusCode):
serverInfo.Status = StatusOnline
serverInfo.ErrorMsg = fmt.Sprintf("%s: request at %s failed", zerr.ErrAPINotSupported.Error(),
checkAPISupportEndpoint)
default:
serverInfo.Status = StatusOffline
serverInfo.ErrorMsg = err.Error()

Check warning on line 109 in pkg/cli/client/server_info_cmd.go

View check run for this annotation

Codecov / codecov/patch

pkg/cli/client/server_info_cmd.go#L107-L109

Added lines #L107 - L109 were not covered by tests
}

return PrintServerInfo(serverInfo, config)
}

func getCredentialsSuggestion(username string) string {
if username == "" {
return "endpoint requires valid user credentials (add the flag '--user [user]:[password]')"
}

return "given credentials are invalid"
}

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
}

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
}
Loading
Loading