Skip to content

Commit

Permalink
Merge pull request #91 from replicatedhq/release-lint
Browse files Browse the repository at this point in the history
implement kots release linting
  • Loading branch information
dexhorthy authored Jan 20, 2020
2 parents 5929e9e + bb7463d commit 5a1982d
Show file tree
Hide file tree
Showing 9 changed files with 145 additions and 115 deletions.
49 changes: 30 additions & 19 deletions cli/cmd/release_create.go
Original file line number Diff line number Diff line change
Expand Up @@ -43,7 +43,9 @@ func (r *runners) InitReleaseCreate(parent *cobra.Command) {
}

func (r *runners) releaseCreate(cmd *cobra.Command, args []string) error {
if r.args.createReleaseYaml == "" && r.args.createReleaseYamlFile == "" && r.args.createReleaseYamlDir == "" {
if r.args.createReleaseYaml == "" &&
r.args.createReleaseYamlFile == "" &&
r.args.createReleaseYamlDir == "" {
return errors.New("one of --yaml, --yaml-file, --yaml-dir is required")
}

Expand Down Expand Up @@ -78,26 +80,11 @@ func (r *runners) releaseCreate(cmd *cobra.Command, args []string) error {
}

if r.args.createReleaseYamlDir != "" {
var allKotsReleaseSpecs []kotsSingleSpec
err := filepath.Walk(r.args.createReleaseYamlDir, func(path string, info os.FileInfo, err error) error {
spec, err := encodeKotsFile(r.args.createReleaseYamlDir, path, info, err)
if err != nil {
return err
} else if spec == nil {
return nil
}
allKotsReleaseSpecs = append(allKotsReleaseSpecs, *spec)
return nil
})
if err != nil {
return errors.Wrapf(err, "walk %s", r.args.createReleaseYamlDir)
}

jsonAllYamls, err := json.Marshal(allKotsReleaseSpecs)
var err error
r.args.createReleaseYaml, err = readYAMLDir(r.args.createReleaseYamlDir)
if err != nil {
return errors.Wrap(err, "marshal spec")
return errors.Wrap(err, "read yaml dir")
}
r.args.createReleaseYaml = string(jsonAllYamls)
}

// if the --promote param was used make sure it identifies exactly one
Expand Down Expand Up @@ -200,3 +187,27 @@ func encodeKotsFile(prefix, path string, info os.FileInfo, err error) (*kotsSing
Children: []string{},
}, nil
}

func readYAMLDir(yamlDir string) (string, error) {

var allKotsReleaseSpecs []kotsSingleSpec
err := filepath.Walk(yamlDir, func(path string, info os.FileInfo, err error) error {
spec, err := encodeKotsFile(yamlDir, path, info, err)
if err != nil {
return err
} else if spec == nil {
return nil
}
allKotsReleaseSpecs = append(allKotsReleaseSpecs, *spec)
return nil
})
if err != nil {
return "", errors.Wrapf(err, "walk %s", yamlDir)
}

jsonAllYamls, err := json.Marshal(allKotsReleaseSpecs)
if err != nil {
return "", errors.Wrap(err, "marshal spec")
}
return string(jsonAllYamls), nil
}
49 changes: 22 additions & 27 deletions cli/cmd/release_lint.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,53 +2,36 @@ package cmd

import (
"fmt"
"io/ioutil"

"github.com/pkg/errors"
"github.com/replicatedhq/replicated/cli/print"
"github.com/spf13/cobra"
)

func (r *runners) InitReleaseLint(parent *cobra.Command) {
cmd := &cobra.Command{
Use: "lint",
Short: "Lint a YAML",
Long: "Lint a YAML",
Short: "Lint a directory of KOTS manifests",
Long: "Lint a directory of KOTS manifests",
SilenceUsage: true,
}
cmd.Hidden=true; // Not supported in KOTS
parent.AddCommand(cmd)

cmd.Flags().StringVar(&r.args.lintReleaseYaml, "yaml", "", "The YAML config to lint. Use '-' to read from stdin. Cannot be used with the `yaml-file` flag.")
cmd.Flags().StringVar(&r.args.lintReleaseYamlFile, "yaml-file", "", "The file name with YAML config to lint. Cannot be used with the `yaml` flag.")
cmd.Flags().StringVar(&r.args.lintReleaseYamlDir, "yaml-dir", "", "The directory containing multiple yamls for a Kots release. Cannot be used with the `yaml` flag.")

cmd.RunE = r.releaseLint
}

func (r *runners) releaseLint(cmd *cobra.Command, args []string) error {
if r.args.lintReleaseYaml == "" && r.args.lintReleaseYamlFile == "" {
if r.args.lintReleaseYamlDir == "" {
return fmt.Errorf("yaml is required")
}

if r.args.lintReleaseYaml != "" && r.args.lintReleaseYamlFile != "" {
return fmt.Errorf("only yaml or yaml-file has to be specified")
}

if r.args.lintReleaseYaml == "-" {
bytes, err := ioutil.ReadAll(r.stdin)
if err != nil {
return err
}
r.args.lintReleaseYaml = string(bytes)
}

if r.args.lintReleaseYamlFile != "" {
bytes, err := ioutil.ReadFile(r.args.lintReleaseYamlFile)
if err != nil {
return err
}
r.args.lintReleaseYamlFile = string(bytes)
lintReleaseYAML, err := readYAMLDir(r.args.lintReleaseYamlDir)
if err != nil {
return errors.Wrap(err, "read yaml dir")
}

lintResult, err := r.api.LintRelease(r.appID, r.appType, r.args.lintReleaseYaml)
lintResult, err := r.api.LintRelease(r.appID, r.appType, lintReleaseYAML)
if err != nil {
return err
}
Expand All @@ -57,5 +40,17 @@ func (r *runners) releaseLint(cmd *cobra.Command, args []string) error {
return err
}

var hasError bool
for _, msg := range lintResult {
if msg.Type == "error" {
hasError = true
break
}
}

if hasError {
return errors.New("one or more errors found")
}

return nil
}
10 changes: 5 additions & 5 deletions cli/cmd/runner.go
Original file line number Diff line number Diff line change
Expand Up @@ -56,8 +56,8 @@ type runnerArgs struct {
createReleasePromoteNotes string
createReleasePromoteVersion string
createReleasePromoteEnsureChannel bool
lintReleaseYaml string
lintReleaseYamlFile string
intReleaseYamlFile string
lintReleaseYamlDir string
releaseOptional bool
releaseNotes string
releaseVersion string
Expand All @@ -78,8 +78,8 @@ type runnerArgs struct {
entitlementsSetValueValue string
entitlementsSetValueType string

customerCreateName string
customerCreateChannel string
customerCreateEnsureChannel bool
customerCreateName string
customerCreateChannel string
customerCreateEnsureChannel bool
customerCreateExpiryDuration time.Duration
}
18 changes: 16 additions & 2 deletions cli/print/lint.go
Original file line number Diff line number Diff line change
Expand Up @@ -7,16 +7,30 @@ import (
"github.com/replicatedhq/replicated/pkg/types"
)

var lintTmplSrc = `RULE TYPE START LINE END LINE
var lintTmplSrc = `RULE TYPE LINE MESSAGE
{{ range . -}}
{{ .Rule }} {{ .Type }} {{ (index .Positions 0).Start.Line }} {{ (index .Positions 0).End.Line }}
{{ .Rule }} {{ .Type }} {{with .Positions}}{{ (index . 0).Start.Line }}{{else}} {{end}} {{ .Message}}
{{ end }}`

var lintTmpl = template.Must(template.New("lint").Parse(lintTmplSrc))

func LintErrors(w *tabwriter.Writer, lintErrors []types.LintMessage) error {
lintErrors = incrementLineNumbersToOneIndexed(lintErrors)
if err := lintTmpl.Execute(w, lintErrors); err != nil {
return err
}
return w.Flush()
}

// line numbers come back zero-indexed from the API
// Since we now attach messages from the yaml parser,
// lets make this 1-indexed so they match if the message includes a line number
// this is here because its primarily client logic, its about how we're displaying the data
func incrementLineNumbersToOneIndexed(lintErrors []types.LintMessage) []types.LintMessage {
for _, lintError := range lintErrors {
if len(lintError.Positions) > 0 {
lintError.Positions[0].Start.Line += 1
}
}
return lintErrors
}
10 changes: 6 additions & 4 deletions client/release.go
Original file line number Diff line number Diff line change
Expand Up @@ -155,15 +155,17 @@ func (c *Client) PromoteRelease(appID string, appType string, sequence int64, la
return errors.New("unknown app type")
}

func (c *Client) LintRelease(appID string, appType string, yaml string) ([]types.LintMessage, error) {
// yamlOrJSON is either a single ship yaml file, or a serialized JSON object describing a yaml-dir, created by readYAMLDir()
// this Client abstraction continue to spring more leaks :)
func (c *Client) LintRelease(appID string, appType string, yamlOrJSON string) ([]types.LintMessage, error) {

if appType == "platform" {
return nil, errors.New("Linting is not yet supported for Platform applications")
// return c.PlatformClient.LintRelease(appID, yaml)
// return c.PlatformClient.LintRelease(appID, yamlOrJSON)
} else if appType == "ship" {
return c.ShipClient.LintRelease(appID, yaml)
return c.ShipClient.LintRelease(appID, yamlOrJSON)
} else if appType == "kots" {
return nil, errors.New("Linting is not yet supported for Kots applications")
return c.KotsClient.LintRelease(appID, yamlOrJSON)
}

return nil, errors.New("unknown app type")
Expand Down
1 change: 1 addition & 0 deletions pkg/kotsclient/client.go
Original file line number Diff line number Diff line change
Expand Up @@ -27,3 +27,4 @@ func NewGraphQLClient(origin string, apiKey string) *GraphQLClient {
func (c *GraphQLClient) ExecuteRequest(requestObj graphql.Request, deserializeTarget interface{}) error {
return c.GraphQLClient.ExecuteRequest(requestObj, deserializeTarget)
}

53 changes: 53 additions & 0 deletions pkg/kotsclient/release_lint.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
package kotsclient

import (
"github.com/pkg/errors"
"github.com/replicatedhq/replicated/pkg/graphql"
"github.com/replicatedhq/replicated/pkg/types"
)

type GraphQLResponseLintRelease struct {
Data *KOTSReleaseLintData `json:"data,omitempty"`
Errors []graphql.GQLError `json:"errors,omitempty"`
}

type KOTSReleaseLintData struct {
Messages []types.LintMessage `json:"lintKotsSpec"`
}

const lintKotsRelease = `
query lintKotsSpec($appId: ID!, $spec: String!) {
lintKotsSpec(appId: $appId, spec: $spec) {
rule
type
message
path
positions {
start {
line
}
}
}
}
`

func (c *GraphQLClient) LintRelease(appID, allKotsYamlsAsJson string) ([]types.LintMessage, error) {

response := GraphQLResponseLintRelease{}

request := graphql.Request{
Query: lintKotsRelease,
OperationName: "lintKotsSpec",
Variables: map[string]interface{}{
"appId": appID,
"spec": allKotsYamlsAsJson,
},
}

if err := c.ExecuteRequest(request, &response); err != nil {
return nil, errors.Wrap(err, "execute request")
}

return response.Data.Messages, nil

}
51 changes: 2 additions & 49 deletions pkg/shipclient/release.go
Original file line number Diff line number Diff line change
Expand Up @@ -59,26 +59,9 @@ type GraphQLResponseLintRelease struct {
}

type ShipReleaseLintData struct {
Messages []*ShipLintMessage `json:"lintRelease"`
Messages []types.LintMessage `json:"lintRelease"`
}

type ShipLintMessage struct {
Rule string `json:"rule"`
Type string `json:"type"`
Positions []*ShipLintPosition `json:"positions"`
}

type ShipLintPosition struct {
Path string `json:"path"`
Start *ShipLintLinePosition `json:"start"`
End *ShipLintLinePosition `json:"end"`
}

type ShipLintLinePosition struct {
Position int64 `json:"position"`
Line int64 `json:"line"`
Column int64 `json:"column"`
}

const listReleasesQuery = `
query allReleases($appId: ID!) {
Expand Down Expand Up @@ -333,35 +316,5 @@ func (c *GraphQLClient) LintRelease(appID string, yaml string) ([]types.LintMess
return nil, err
}

lintMessages := make([]types.LintMessage, 0, 0)
for _, message := range response.Data.Messages {
positions := make([]*types.LintPosition, 0, 0)
for _, lintPosition := range message.Positions {
position := types.LintPosition{
Path: lintPosition.Path,
Start: &types.LintLinePosition{
Position: lintPosition.Start.Position,
Column: lintPosition.Start.Column,
Line: lintPosition.Start.Line,
},
End: &types.LintLinePosition{
Position: lintPosition.End.Position,
Column: lintPosition.End.Column,
Line: lintPosition.End.Line,
},
}

positions = append(positions, &position)
}

lintMessage := types.LintMessage{
Rule: message.Rule,
Type: message.Type,
Positions: positions,
}

lintMessages = append(lintMessages, lintMessage)
}

return lintMessages, nil
return response.Data.Messages, nil
}
19 changes: 10 additions & 9 deletions pkg/types/release.go
Original file line number Diff line number Diff line change
Expand Up @@ -13,19 +13,20 @@ type ReleaseInfo struct {
}

type LintMessage struct {
Rule string
Type string
Positions []*LintPosition
Rule string `json:"rule"`
Type string `json:"type"`
Message string `json:"message"`
Positions []*LintPosition `json:"positions"`
}

type LintPosition struct {
Path string
Start *LintLinePosition
End *LintLinePosition
Path string `json:"path"`
Start LintLinePosition `json:"start"`
End LintLinePosition `json:"end"`
}

type LintLinePosition struct {
Position int64
Line int64
Column int64
Position int64 `json:"position"`
Line int64 `json:"line"`
Column int64 `json:"column"`
}

0 comments on commit 5a1982d

Please sign in to comment.