get.porter.sh/porter@v1.3.0/pkg/linter/linter.go (about) 1 package linter 2 3 import ( 4 "context" 5 "encoding/json" 6 "fmt" 7 "strings" 8 9 "get.porter.sh/porter/pkg/config" 10 "get.porter.sh/porter/pkg/manifest" 11 "get.porter.sh/porter/pkg/mixin/query" 12 "get.porter.sh/porter/pkg/pkgmgmt" 13 "get.porter.sh/porter/pkg/portercontext" 14 "get.porter.sh/porter/pkg/tracing" 15 "get.porter.sh/porter/pkg/yaml" 16 "github.com/Masterminds/semver/v3" 17 "github.com/dustin/go-humanize" 18 ) 19 20 // Level of severity for a lint result. 21 type Level int 22 23 func (l Level) String() string { 24 switch l { 25 case LevelError: 26 return "error" 27 case LevelWarning: 28 return "warning" 29 } 30 return "" 31 } 32 33 // Code representing the problem identified by the linter 34 // Recommended to use the pattern MIXIN-NUMBER so that you don't collide with 35 // codes from another mixin or with Porter's codes. 36 // Example: 37 // - exec-105 38 // - helm-410 39 type Code string 40 41 const ( 42 // LevelError indicates a lint result is an error that will prevent the bundle from building properly. 43 LevelError Level = 0 44 45 // LevelWarning indicates a lint result is a warning about a best practice or identifies a problem that is not 46 // guaranteed to break the build. 47 LevelWarning Level = 2 48 ) 49 50 // Result is a single item identified by the linter. 51 type Result struct { 52 // Level of severity 53 Level Level 54 55 // Location of the problem in the manifest. 56 Location Location 57 58 // Code uniquely identifying the type of problem. 59 Code Code 60 61 // Title to display (80 chars). 62 Title string 63 64 // Message explaining the problem. 65 Message string 66 67 // URL that provides additional assistance with this problem. 68 URL string 69 } 70 71 func (r Result) String() string { 72 var buffer strings.Builder 73 buffer.WriteString(fmt.Sprintf("%s(%s) - %s\n", r.Level, r.Code, r.Title)) 74 if r.Location.Mixin != "" { 75 buffer.WriteString(r.Location.String() + "\n") 76 } 77 78 if r.Message != "" { 79 buffer.WriteString(r.Message + "\n") 80 } 81 82 if r.URL != "" { 83 buffer.WriteString(fmt.Sprintf("See %s for more information\n", r.URL)) 84 } 85 86 buffer.WriteString("---\n") 87 return buffer.String() 88 } 89 90 // Location identifies the offending mixin step within a manifest. 91 type Location struct { 92 // Action containing the step, e.g. Install. 93 Action string 94 95 // Mixin name, e.g. exec. 96 Mixin string 97 98 // StepNumber is the position of the step, starting from 1, within the action. 99 // Example 100 // install: 101 // - exec: (1) 102 // ... 103 // - helm3: (2) 104 // ... 105 // - exec: (3) 106 // ... 107 StepNumber int 108 109 // StepDescription is the description of the step provided in the manifest. 110 // Example 111 // install: 112 // - exec: 113 // description: THIS IS THE STEP DESCRIPTION 114 // command: ./helper.sh 115 StepDescription string 116 } 117 118 func (l Location) String() string { 119 return fmt.Sprintf("%s: %s step in the %s mixin (%s)", 120 l.Action, humanize.Ordinal(l.StepNumber), l.Mixin, l.StepDescription) 121 } 122 123 // Results is a set of items identified by the linter. 124 type Results []Result 125 126 func (r Results) String() string { 127 var buffer strings.Builder 128 // TODO: Sort, display errors first 129 for _, result := range r { 130 buffer.WriteString(result.String()) 131 } 132 133 return buffer.String() 134 } 135 136 // HasError checks if any of the results is an error. 137 func (r Results) HasError() bool { 138 for _, result := range r { 139 if result.Level == LevelError { 140 return true 141 } 142 } 143 return false 144 } 145 146 // Linter manages executing the lint command for all affected mixins and reporting 147 // the results. 148 type Linter struct { 149 *portercontext.Context 150 Mixins pkgmgmt.PackageManager 151 } 152 153 func New(cxt *portercontext.Context, mixins pkgmgmt.PackageManager) *Linter { 154 return &Linter{ 155 Context: cxt, 156 Mixins: mixins, 157 } 158 } 159 160 type action struct { 161 name string 162 steps manifest.Steps 163 } 164 165 func (l *Linter) Lint(ctx context.Context, m *manifest.Manifest, config *config.Config) (Results, error) { 166 // Check for reserved porter prefix on parameter names 167 reservedPrefixes := []string{"porter-", "porter_"} 168 params := m.Parameters 169 170 var results Results 171 172 for _, param := range params { 173 paramName := strings.ToLower(param.Name) 174 for _, reservedPrefix := range reservedPrefixes { 175 if strings.HasPrefix(paramName, reservedPrefix) { 176 177 res := Result{ 178 Level: LevelError, 179 Location: Location{ 180 Action: "", 181 Mixin: "", 182 StepNumber: 0, 183 StepDescription: "", 184 }, 185 Code: "porter-100", 186 Title: "Reserved name error", 187 Message: param.Name + " has a reserved prefix. Parameters cannot start with porter- or porter_", 188 URL: "https://porter.sh/reference/linter/#porter-100", 189 } 190 results = append(results, res) 191 } 192 } 193 } 194 195 // Check if parameters apply to the steps 196 ctx, span := tracing.StartSpan(ctx) 197 defer span.EndSpan() 198 199 span.Debug("Validating that parameters applies to the actions...") 200 tmplParams := m.GetTemplatedParameters() 201 actions := []action{ 202 {"install", m.Install}, 203 {"upgrade", m.Upgrade}, 204 {"uninstall", m.Uninstall}, 205 } 206 for actionName, steps := range m.CustomActions { 207 actions = append(actions, action{actionName, steps}) 208 } 209 for _, action := range actions { 210 res, err := validateParamsAppliesToAction(m, action.steps, tmplParams, action.name, config) 211 if err != nil { 212 return nil, span.Error(fmt.Errorf("error validating action: %s", action.name)) 213 } 214 results = append(results, res...) 215 } 216 217 deps := make(map[string]interface{}, len(m.Dependencies.Requires)) 218 for _, dep := range m.Dependencies.Requires { 219 if _, exists := deps[dep.Name]; exists { 220 res := Result{ 221 Level: LevelError, 222 Location: Location{ 223 Action: "", 224 Mixin: "", 225 StepNumber: 0, 226 StepDescription: "", 227 }, 228 Code: "porter-102", 229 Title: "Dependency error", 230 Message: fmt.Sprintf("The dependency %s is defined multiple times", dep.Name), 231 URL: "https://porter.sh/reference/linter/#porter-102", 232 } 233 results = append(results, res) 234 } else { 235 deps[dep.Name] = nil 236 } 237 } 238 239 span.Debug("Running linters for each mixin used in the manifest...") 240 q := query.New(l.Context, l.Mixins) 241 responses, err := q.Execute(ctx, "lint", query.NewManifestGenerator(m)) 242 if err != nil { 243 return nil, span.Error(err) 244 } 245 246 for _, response := range responses { 247 if response.Error != nil { 248 // Ignore mixins that do not support the lint command 249 if strings.Contains(response.Error.Error(), "unknown command") { 250 continue 251 } 252 // put a helpful error when the mixin is not installed 253 if strings.Contains(response.Error.Error(), "not installed") { 254 return nil, span.Error(fmt.Errorf("mixin %[1]s is not currently installed. To find view more details you can run: porter mixin search %[1]s. To install you can run porter mixin install %[1]s", response.Name)) 255 } 256 return nil, span.Error(fmt.Errorf("lint command failed for mixin %s: %s", response.Name, response.Stdout)) 257 } 258 259 var r Results 260 err = json.Unmarshal([]byte(response.Stdout), &r) 261 if err != nil { 262 return nil, span.Error(fmt.Errorf("unable to parse lint response from mixin %s: %w", response.Name, err)) 263 } 264 265 results = append(results, r...) 266 } 267 268 span.Debug("Getting versions for each mixin used in the manifest...") 269 err = l.validateVersionNumberConstraints(ctx, m) 270 if err != nil { 271 return nil, span.Error(err) 272 } 273 274 return results, nil 275 } 276 277 func (l *Linter) validateVersionNumberConstraints(ctx context.Context, m *manifest.Manifest) error { 278 for _, mixin := range m.Mixins { 279 if mixin.Version != nil { 280 installedMeta, err := l.Mixins.GetMetadata(ctx, mixin.Name) 281 if err != nil { 282 return fmt.Errorf("unable to get metadata from mixin %s: %w", mixin.Name, err) 283 } 284 installedVersion := installedMeta.GetVersionInfo().Version 285 286 err = validateSemverConstraint(mixin.Name, installedVersion, mixin.Version) 287 if err != nil { 288 return err 289 } 290 } 291 } 292 293 return nil 294 } 295 296 func validateSemverConstraint(name string, installedVersion string, versionConstraint *semver.Constraints) error { 297 v, err := semver.NewVersion(installedVersion) 298 if err != nil { 299 return fmt.Errorf("invalid version number from mixin %s: %s. %w", name, installedVersion, err) 300 } 301 302 if !versionConstraint.Check(v) { 303 return fmt.Errorf("mixin %s is installed at version %s but your bundle requires version %s", name, installedVersion, versionConstraint) 304 } 305 return nil 306 } 307 308 func validateParamsAppliesToAction(m *manifest.Manifest, steps manifest.Steps, tmplParams manifest.ParameterDefinitions, actionName string, config *config.Config) (Results, error) { 309 var results Results 310 for stepNumber, step := range steps { 311 data, err := yaml.Marshal(step.Data) 312 if err != nil { 313 return nil, fmt.Errorf("error during marshalling: %w", err) 314 } 315 316 tmplResult, err := m.ScanManifestTemplating(data, config) 317 if err != nil { 318 return nil, fmt.Errorf("error parsing templating: %w", err) 319 } 320 321 for _, variable := range tmplResult.Variables { 322 paramName, ok := m.GetTemplateParameterName(variable) 323 if !ok { 324 continue 325 } 326 327 for _, tmplParam := range tmplParams { 328 if tmplParam.Name != paramName { 329 continue 330 } 331 if !tmplParam.AppliesTo(actionName) { 332 description, err := step.GetDescription() 333 if err != nil { 334 return nil, fmt.Errorf("error getting step description: %w", err) 335 } 336 res := Result{ 337 Level: LevelError, 338 Location: Location{ 339 Action: actionName, 340 Mixin: step.GetMixinName(), 341 StepNumber: stepNumber + 1, 342 StepDescription: description, 343 }, 344 Code: "porter-101", 345 Title: "Parameter does not apply to action", 346 Message: fmt.Sprintf("Parameter %s does not apply to %s action", paramName, actionName), 347 URL: "https://porter.sh/docs/references/linter/#porter-101", 348 } 349 results = append(results, res) 350 } 351 } 352 } 353 } 354 355 return results, nil 356 }