This repository has been archived by the owner on Oct 30, 2024. It is now read-only.
-
Notifications
You must be signed in to change notification settings - Fork 189
✨ Adds "deprecated apis" auditor #428
Merged
Merged
Changes from all commits
Commits
Show all changes
7 commits
Select commit
Hold shift + click to select a range
d8fdaa2
add DeprecatedAPI auditor
jerr 6d22d1f
Add documentation
jerr b66c84c
fix typo
jerr 792ff20
Ignore warnings from kubeclient
jerr c6067fe
Update documentation and fix typos
jerr 1bc1269
Add AuditLocal test
jerr e51eea1
use client-go/kubernetes/scheme
jerr File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,50 @@ | ||
package deprecatedapis | ||
|
||
import ( | ||
"fmt" | ||
"regexp" | ||
"strconv" | ||
) | ||
|
||
type Config struct { | ||
CurrentVersion string `yaml:"currentVersion"` | ||
TargetedVersion string `yaml:"targetedVersion"` | ||
} | ||
|
||
type Version struct { | ||
Major int | ||
Minor int | ||
} | ||
|
||
func (config *Config) GetCurrentVersion() (*Version, error) { | ||
if config == nil { | ||
return nil, nil | ||
} | ||
return toMajorMinor(config.CurrentVersion) | ||
} | ||
|
||
func (config *Config) GetTargetedVersion() (*Version, error) { | ||
if config == nil { | ||
return nil, nil | ||
} | ||
return toMajorMinor(config.TargetedVersion) | ||
} | ||
|
||
func toMajorMinor(version string) (*Version, error) { | ||
if len(version) == 0 { | ||
return nil, nil | ||
} | ||
re := regexp.MustCompile(`^(\d{1,2})\.(\d{1,2})$`) | ||
if !re.MatchString(version) { | ||
return nil, fmt.Errorf("error parsing version: %s", version) | ||
} | ||
major, err := strconv.Atoi(re.FindStringSubmatch(version)[1]) | ||
if err != nil { | ||
return nil, err | ||
} | ||
minor, err := strconv.Atoi(re.FindStringSubmatch(version)[2]) | ||
if err != nil { | ||
return nil, err | ||
} | ||
return &Version{major, minor}, nil | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,133 @@ | ||
package deprecatedapis | ||
|
||
import ( | ||
"fmt" | ||
"strconv" | ||
|
||
"github.com/Shopify/kubeaudit" | ||
"github.com/Shopify/kubeaudit/internal/k8sinternal" | ||
"github.com/Shopify/kubeaudit/pkg/k8s" | ||
v1 "k8s.io/api/core/v1" | ||
"k8s.io/apimachinery/pkg/runtime/schema" | ||
) | ||
|
||
const Name = "deprecatedapis" | ||
|
||
const ( | ||
// DeprecatedAPIUsed occurs when a deprecated resource type version is used | ||
DeprecatedAPIUsed = "DeprecatedAPIUsed" | ||
) | ||
|
||
// DeprecatedAPIs implements Auditable | ||
type DeprecatedAPIs struct { | ||
CurrentVersion *Version | ||
TargetedVersion *Version | ||
} | ||
|
||
func New(config Config) (*DeprecatedAPIs, error) { | ||
currentVersion, err := config.GetCurrentVersion() | ||
if err != nil { | ||
return nil, fmt.Errorf("error creating DeprecatedAPIs auditor: %w", err) | ||
} | ||
|
||
targetedVersion, err := config.GetTargetedVersion() | ||
if err != nil { | ||
return nil, fmt.Errorf("error creating DeprecatedAPIs auditor: %w", err) | ||
} | ||
|
||
return &DeprecatedAPIs{ | ||
CurrentVersion: currentVersion, | ||
TargetedVersion: targetedVersion, | ||
}, nil | ||
} | ||
|
||
// APILifecycleDeprecated is a generated function on the available APIs, returning the release in which the API struct was or will be deprecated as int versions of major and minor for comparison. | ||
// https://github.com/kubernetes/code-generator/blob/v0.24.1/cmd/prerelease-lifecycle-gen/prerelease-lifecycle-generators/status.go#L475-L479 | ||
type apiLifecycleDeprecated interface { | ||
APILifecycleDeprecated() (major, minor int) | ||
} | ||
|
||
// APILifecycleRemoved is a generated function on the available APIs, returning the release in which the API is no longer served as int versions of major and minor for comparison. | ||
// https://github.com/kubernetes/code-generator/blob/v0.24.1/cmd/prerelease-lifecycle-gen/prerelease-lifecycle-generators/status.go#L491-L495 | ||
type apiLifecycleRemoved interface { | ||
APILifecycleRemoved() (major, minor int) | ||
} | ||
|
||
// APILifecycleReplacement is a generated function on the available APIs, returning the group, version, and kind that should be used instead of this deprecated type. | ||
// https://github.com/kubernetes/code-generator/blob/v0.24.1/cmd/prerelease-lifecycle-gen/prerelease-lifecycle-generators/status.go#L482-L487 | ||
type apiLifecycleReplacement interface { | ||
APILifecycleReplacement() schema.GroupVersionKind | ||
} | ||
|
||
// APILifecycleIntroduced is a generated function on the available APIs, returning the release in which the API struct was introduced as int versions of major and minor for comparison. | ||
// https://github.com/kubernetes/code-generator/blob/v0.24.1/cmd/prerelease-lifecycle-gen/prerelease-lifecycle-generators/status.go#L467-L473 | ||
type apiLifecycleIntroduced interface { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Is this interface used anywhere? |
||
APILifecycleIntroduced() (major, minor int) | ||
} | ||
|
||
// Audit checks that the resource API version is not deprecated | ||
func (deprecatedAPIs *DeprecatedAPIs) Audit(resource k8s.Resource, _ []k8s.Resource) ([]*kubeaudit.AuditResult, error) { | ||
var auditResults []*kubeaudit.AuditResult | ||
lastApplied, ok := k8s.GetAnnotations(resource)[v1.LastAppliedConfigAnnotation] | ||
if ok && len(lastApplied) > 0 { | ||
resource, _ = k8sinternal.DecodeResource([]byte(lastApplied)) | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Why are we ignoring this error? |
||
} | ||
deprecated, isDeprecated := resource.(apiLifecycleDeprecated) | ||
if isDeprecated { | ||
deprecatedMajor, deprecatedMinor := deprecated.APILifecycleDeprecated() | ||
if deprecatedMajor == 0 && deprecatedMinor == 0 { | ||
return nil, fmt.Errorf("version not found %s (%d.%d)", deprecated, deprecatedMajor, deprecatedMinor) | ||
} else { | ||
severity := kubeaudit.Warn | ||
metadata := kubeaudit.Metadata{ | ||
"DeprecatedMajor": strconv.Itoa(deprecatedMajor), | ||
"DeprecatedMinor": strconv.Itoa(deprecatedMinor), | ||
} | ||
if deprecatedAPIs.CurrentVersion != nil && (deprecatedAPIs.CurrentVersion.Major < deprecatedMajor || deprecatedAPIs.CurrentVersion.Major == deprecatedMajor && deprecatedAPIs.CurrentVersion.Minor < deprecatedMinor) { | ||
severity = kubeaudit.Info | ||
} | ||
gvk := resource.GetObjectKind().GroupVersionKind() | ||
if gvk.Empty() { | ||
return nil, fmt.Errorf("GroupVersionKind not found %s", resource) | ||
} else { | ||
deprecationMessage := fmt.Sprintf("%s %s is deprecated in v%d.%d+", gvk.GroupVersion().String(), gvk.Kind, deprecatedMajor, deprecatedMinor) | ||
if removed, hasRemovalInfo := resource.(apiLifecycleRemoved); hasRemovalInfo { | ||
removedMajor, removedMinor := removed.APILifecycleRemoved() | ||
if removedMajor != 0 || removedMinor != 0 { | ||
deprecationMessage = deprecationMessage + fmt.Sprintf(", unavailable in v%d.%d+", removedMajor, removedMinor) | ||
metadata["RemovedMajor"] = strconv.Itoa(removedMajor) | ||
metadata["RemovedMinor"] = strconv.Itoa(removedMinor) | ||
} | ||
if deprecatedAPIs.TargetedVersion != nil && deprecatedAPIs.TargetedVersion.Major >= removedMajor && deprecatedAPIs.TargetedVersion.Minor >= removedMinor { | ||
severity = kubeaudit.Error | ||
} | ||
} | ||
if introduced, hasIntroduced := resource.(apiLifecycleIntroduced); hasIntroduced { | ||
introducedMajor, introducedMinor := introduced.APILifecycleIntroduced() | ||
if introducedMajor != 0 || introducedMinor != 0 { | ||
deprecationMessage = deprecationMessage + fmt.Sprintf(", introduced in v%d.%d+", introducedMajor, introducedMinor) | ||
metadata["IntroducedMajor"] = strconv.Itoa(introducedMajor) | ||
metadata["IntroducedMinor"] = strconv.Itoa(introducedMinor) | ||
} | ||
} | ||
if replaced, hasReplacement := resource.(apiLifecycleReplacement); hasReplacement { | ||
replacement := replaced.APILifecycleReplacement() | ||
if !replacement.Empty() { | ||
deprecationMessage = deprecationMessage + fmt.Sprintf("; use %s %s", replacement.GroupVersion().String(), replacement.Kind) | ||
metadata["ReplacementGroup"] = replacement.GroupVersion().String() | ||
metadata["ReplacementKind"] = replacement.Kind | ||
} | ||
} | ||
auditResult := &kubeaudit.AuditResult{ | ||
Name: DeprecatedAPIUsed, | ||
Severity: severity, | ||
Message: deprecationMessage, | ||
Metadata: metadata, | ||
} | ||
auditResults = append(auditResults, auditResult) | ||
} | ||
} | ||
|
||
} | ||
return auditResults, nil | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,72 @@ | ||
package deprecatedapis | ||
|
||
import ( | ||
"fmt" | ||
"strings" | ||
"testing" | ||
|
||
"github.com/Shopify/kubeaudit" | ||
"github.com/Shopify/kubeaudit/internal/test" | ||
"github.com/stretchr/testify/assert" | ||
"github.com/stretchr/testify/require" | ||
) | ||
|
||
const fixtureDir = "fixtures" | ||
|
||
func TestAuditDeprecatedAPIs(t *testing.T) { | ||
cases := []struct { | ||
file string | ||
currentVersion string | ||
targetedVersion string | ||
expectedSeverity kubeaudit.SeverityLevel | ||
}{ | ||
{"cronjob.yml", "", "", kubeaudit.Warn}, // Warn is the serverity by default | ||
{"cronjob.yml", "1.20", "1.21", kubeaudit.Info}, // Info, not yet deprecated in the current version | ||
{"cronjob.yml", "1.21", "1.22", kubeaudit.Warn}, // Warn, deprecated in the current version | ||
{"cronjob.yml", "1.22", "1.25", kubeaudit.Error}, // Error, not available in the targeted version | ||
{"cronjob.yml", "1.20", "1.25", kubeaudit.Error}, // Error, not yet deprecated in the current version but not available in the targeted version | ||
{"cronjob.yml", "1.20", "", kubeaudit.Info}, // Info, not yet deprecated in the current version and no targeted version defined | ||
{"cronjob.yml", "1.21", "", kubeaudit.Warn}, // Warn, deprecated in the current version | ||
{"cronjob.yml", "", "1.20", kubeaudit.Warn}, // Warn is the serverity by default if no current version | ||
{"cronjob.yml", "", "1.25", kubeaudit.Error}, // Error, not available in the targeted version | ||
} | ||
|
||
message := "batch/v1beta1 CronJob is deprecated in v1.21+, unavailable in v1.25+, introduced in v1.8+; use batch/v1 CronJob" | ||
metadata := kubeaudit.Metadata{ | ||
"DeprecatedMajor": "1", | ||
"DeprecatedMinor": "21", | ||
"RemovedMajor": "1", | ||
"RemovedMinor": "25", | ||
"IntroducedMajor": "1", | ||
"IntroducedMinor": "8", | ||
"ReplacementGroup": "batch/v1", | ||
"ReplacementKind": "CronJob", | ||
} | ||
|
||
for i, tc := range cases { | ||
// These lines are needed because of how scopes work with parallel tests (see https://gist.github.com/posener/92a55c4cd441fc5e5e85f27bca008721) | ||
tc := tc | ||
i := i | ||
t.Run(tc.file+"-"+tc.currentVersion+"-"+tc.targetedVersion, func(t *testing.T) { | ||
t.Parallel() | ||
auditor, err := New(Config{CurrentVersion: tc.currentVersion, TargetedVersion: tc.targetedVersion}) | ||
require.Nil(t, err) | ||
report := test.AuditManifest(t, fixtureDir, tc.file, auditor, []string{DeprecatedAPIUsed}) | ||
assertReport(t, report, tc.expectedSeverity, message, metadata) | ||
report = test.AuditLocal(t, fixtureDir, tc.file, auditor, fmt.Sprintf("%s-%d", strings.Split(tc.file, ".")[0], i), []string{DeprecatedAPIUsed}) | ||
assertReport(t, report, tc.expectedSeverity, message, metadata) | ||
}) | ||
} | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Is it possible to also |
||
} | ||
|
||
func assertReport(t *testing.T, report *kubeaudit.Report, expectedSeverity kubeaudit.SeverityLevel, message string, metadata map[string]string) { | ||
assert.Equal(t, 1, len(report.Results())) | ||
for _, result := range report.Results() { | ||
assert.Equal(t, 1, len(result.GetAuditResults())) | ||
for _, auditResult := range result.GetAuditResults() { | ||
require.Equal(t, expectedSeverity, auditResult.Severity) | ||
require.Equal(t, message, auditResult.Message) | ||
require.Equal(t, metadata, auditResult.Metadata) | ||
} | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,19 @@ | ||
apiVersion: batch/v1beta1 | ||
kind: CronJob | ||
metadata: | ||
name: hello | ||
spec: | ||
schedule: "* * * * *" | ||
jobTemplate: | ||
spec: | ||
template: | ||
spec: | ||
containers: | ||
- name: hello | ||
image: busybox | ||
imagePullPolicy: IfNotPresent | ||
command: | ||
- /bin/sh | ||
- -c | ||
- date; echo Hello from the Kubernetes cluster | ||
restartPolicy: OnFailure |
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Could we maybe add a comment with a reference link for these functions? I assume they're defined somewhere in the kubernetes codebase?