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

feat: add support to validate via Kong API #502

Merged
merged 20 commits into from
Jan 19, 2022
Merged
Show file tree
Hide file tree
Changes from 1 commit
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
13 changes: 13 additions & 0 deletions cmd/common.go
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,19 @@ var (
assumeYes bool
)

func performValidate(ctx context.Context, kongClient *kong.Client, entity string) (bool, error) {
endpoint := fmt.Sprintf("/schemas/%s/validate", entity)
req, err := kongClient.NewRequest("POST", endpoint, nil, nil)
GGabriele marked this conversation as resolved.
Show resolved Hide resolved
if err != nil {
return false, err
}
resp, err := kongClient.Do(ctx, req, nil)
if err != nil {
return false, err
GGabriele marked this conversation as resolved.
Show resolved Hide resolved
}
return resp.StatusCode == http.StatusOK, nil
}

// workspaceExists checks if workspace exists in Kong.
func workspaceExists(ctx context.Context, config utils.KongClientConfig, workspaceName string) (bool, error) {
rootConfig := config.ForWorkspace("")
Expand Down
120 changes: 88 additions & 32 deletions cmd/validate.go
Original file line number Diff line number Diff line change
@@ -1,71 +1,122 @@
package cmd

import (
"context"
"fmt"
"sync"

"github.com/fatih/color"
"github.com/kong/deck/file"
"github.com/kong/deck/state"
"github.com/kong/deck/utils"
"github.com/kong/go-kong/kong"
"github.com/spf13/cobra"
)

var (
validateCmdKongStateFile []string
validateCmdRBACResourcesOnly bool
validateCmdKongAPI bool
validateCmdKongAPIEntity []string
)

// validateCmd represents the diff command
var validateCmd = &cobra.Command{
Use: "validate",
Short: "Validate the state file",
Long: `The validate command reads the state file and ensures validity.
Short: "Validate configuration",
Long: `The validate command supports 2 modes to ensure validity:
- reads the state file (default)
- queries Kong API

It reads all the specified state files and reports YAML/JSON
By default, it reads all the specified state files and reports YAML/JSON
parsing issues. It also checks for foreign relationships
and alerts if there are broken relationships, or missing links present.
No communication takes places between decK and Kong during the execution of
this command.

When ran with the --use-kong-api flag, it uses the 'validate' endpoint in Kong
to validate entities configuration.
`,
Args: validateNoArgs,
RunE: func(cmd *cobra.Command, args []string) error {
_ = sendAnalytics("validate", "")
// read target file
// this does json schema validation as well
targetContent, err := file.GetContentFromFiles(validateCmdKongStateFile)
if err != nil {
return err
}

dummyEmptyState, err := state.NewKongState()
if err != nil {
return err
}

rawState, err := file.Get(targetContent, file.RenderConfig{
CurrentState: dummyEmptyState,
})
if err != nil {
return err
if validateCmdKongAPI {
return validateWithKong(cmd, validateCmdKongAPIEntity)
}
if err := checkForRBACResources(*rawState, validateCmdRBACResourcesOnly); err != nil {
return err
}
// this catches foreign relation errors
_, err = state.Get(rawState)
if err != nil {
return err
}

return nil
return validateWithFile()
},
PreRunE: func(cmd *cobra.Command, args []string) error {
if len(validateCmdKongStateFile) == 0 {
return fmt.Errorf("a state file with Kong's configuration " +
"must be specified using -s/--state flag")
} else if validateCmdKongAPI && len(validateCmdKongAPIEntity) == 0 {
return fmt.Errorf("a list of entities must be specified " +
"using the -e/--entity flag when validating via Kong API " +
"(e.g. -e entity1 -e entity2)")
}
return nil
},
}

func validate(ctx context.Context, entity string, kongClient *kong.Client) string {
_, err := performValidate(ctx, kongClient, entity)
output := color.New(color.FgGreen, color.Bold).Sprintf("%s: schema validation successful", entity)
if err != nil {
output = color.New(color.FgRed, color.Bold).Sprintf("%s: %v", entity, err)
}
return output
}
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Get rid off this function and call validateEntity from validateEntities directly.


func validateWithKong(cmd *cobra.Command, entity []string) error {
ctx := cmd.Context()
kongClient, err := utils.GetKongClient(rootConfig)
if err != nil {
return err
}
var wg sync.WaitGroup
wg.Add(len(entity))

for _, e := range entity {
go func(e string) {
defer wg.Done()
output := validate(ctx, e, kongClient)
fmt.Println(output)
}(e)
}
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

My one last product review comment: it must be possible for the user to validate their configuration without the existence of a workspace. It is usual for our users to validate their configuration before applying. This is a blocker for merging.
cc @rainest can help figure out alternative solutions

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Some workspace has to exist for you to check :)

You could have this fall back to trying against default if the workspace requested isn't available. It may fail, as you may not have permissions--using --workspace here implies you don't, since otherwise you could omit the flag and check against default fine.

Users may try and just always supply --workspace if that's their intended target for lack of understanding that it's only needed here to work around RBAC issues, since that's a fairly nuanced point.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@rainest Can we open a Github issue with the relevant context to track this problem? We must improve this later if not in this PR.
The PR can be merged after that.

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

So deck already assumes the default workspace always exists: https://github.com/Kong/deck/blob/main/cmd/common.go#L31-L35

What we can do is simply overwriting the user input from --workspace and set it to the default while printing a warning:

	if !workspaceExists {
		cprint.DeletePrintf("Warning: Workspace '%v' specified via --workspace flag "+
			"doesn't exist. Validating against default workspace instead.\n", workspaceName)
		workspaceName = ""
	}
$ deck validate --online
validate entity 'services': HTTP status 400 (message: "schema violation (host: must not have a port)")
$ deck validate --online --workspace randomWS
Warning: Workspace 'randomWS' specified via --workspace flag is different from workspace '' found in state file(s).
Warning: Workspace 'randomWS' specified via --workspace flag doesn't exist. Validating against default workspace instead.
validate entity 'services': HTTP status 400 (message: "schema violation (host: must not have a port)")

If that is fine to you, I can push the changes, so that we don't have to open a different issue and tackle it at a different time.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@GGabriele Please open an issue to track this problem. Can skip for now.

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

wg.Wait()

return nil
}

func validateWithFile() error {
// read target file
// this does json schema validation as well
targetContent, err := file.GetContentFromFiles(validateCmdKongStateFile)
if err != nil {
return err
}

dummyEmptyState, err := state.NewKongState()
if err != nil {
return err
}

rawState, err := file.Get(targetContent, file.RenderConfig{
CurrentState: dummyEmptyState,
})
if err != nil {
return err
}
if err := checkForRBACResources(*rawState, validateCmdRBACResourcesOnly); err != nil {
return err
}
// this catches foreign relation errors
_, err = state.Get(rawState)
if err != nil {
return err
}

return nil
}

func init() {
rootCmd.AddCommand(validateCmd)
validateCmd.Flags().BoolVar(&validateCmdRBACResourcesOnly, "rbac-resources-only",
Expand All @@ -74,4 +125,9 @@ func init() {
"state", "s", []string{"kong.yaml"}, "file(s) containing Kong's configuration.\n"+
"This flag can be specified multiple times for multiple files.\n"+
"Use '-' to read from stdin.")
validateCmd.Flags().BoolVar(&validateCmdKongAPI, "use-kong-api",
false, "whether to leverage Kong's API to validate configuration.")
validateCmd.Flags().StringSliceVarP(&validateCmdKongAPIEntity,
"entity", "e", []string{}, "entities to be validated via Kong API "+
"(e.g. -e entity1 -e entity2)")
GGabriele marked this conversation as resolved.
Show resolved Hide resolved
}
1 change: 1 addition & 0 deletions main.go
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ func registerSignalHandler() context.Context {
signal.Notify(sigs, syscall.SIGINT, syscall.SIGTERM)

go func() {
defer signal.Stop(sigs)
GGabriele marked this conversation as resolved.
Show resolved Hide resolved
sig := <-sigs
fmt.Println("received", sig, ", terminating...")
cancel()
Expand Down