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
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
✨ Adds "deprecated apis" auditor (#428)
- Loading branch information
Showing
14 changed files
with
479 additions
and
87 deletions.
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 { | ||
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)) | ||
} | ||
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) | ||
}) | ||
} | ||
} | ||
|
||
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.