diff --git a/common-lib/pubsub-lib/JetStreamUtil.go b/common-lib/pubsub-lib/JetStreamUtil.go index cfd7f98e4..840bc39e5 100644 --- a/common-lib/pubsub-lib/JetStreamUtil.go +++ b/common-lib/pubsub-lib/JetStreamUtil.go @@ -139,6 +139,9 @@ const ( STORAGE_VELERO_POST_INSTALLATION_TOPIC string = "STORAGE_VELERO_POST_INSTALLATION_TOPIC" STORAGE_VELERO_POST_INSTALLATION_GROUP string = "STORAGE_VELERO_POST_INSTALLATION_GROUP" STORAGE_VELERO_POST_INSTALLATION_DURABLE string = "STORAGE_VELERO_POST_INSTALLATION_DURABLE" + AUDIT_LOG_TOPIC string = "AUDIT_LOG_TOPIC" + AUDIT_LOG_GROUP string = "AUDIT_LOG_GROUP" + AUDIT_LOG_DURABLE string = "AUDIT_LOG_DURABLE" ) type NatsTopic struct { @@ -199,6 +202,7 @@ var natsTopicMapping = map[string]NatsTopic{ STORAGE_MODULE_TOPIC: {topicName: STORAGE_MODULE_TOPIC, streamName: ORCHESTRATOR_STREAM, queueName: STORAGE_MODULE_GROUP, consumerName: STORAGE_MODULE_DURABLE}, STORAGE_VELERO_INSTALL_TOPIC: {topicName: STORAGE_VELERO_INSTALL_TOPIC, streamName: ORCHESTRATOR_STREAM, queueName: STORAGE_VELERO_INSTALL_GROUP, consumerName: STORAGE_VELERO_INSTALL_DURABLE}, STORAGE_VELERO_POST_INSTALLATION_TOPIC: {topicName: STORAGE_VELERO_POST_INSTALLATION_TOPIC, streamName: ORCHESTRATOR_STREAM, queueName: STORAGE_VELERO_POST_INSTALLATION_GROUP, consumerName: STORAGE_VELERO_POST_INSTALLATION_DURABLE}, + AUDIT_LOG_TOPIC: {topicName: AUDIT_LOG_TOPIC, streamName: ORCHESTRATOR_STREAM, queueName: AUDIT_LOG_GROUP, consumerName: AUDIT_LOG_DURABLE}, } var NatsStreamWiseConfigMapping = map[string]NatsStreamConfig{ @@ -246,6 +250,7 @@ var NatsConsumerWiseConfigMapping = map[string]NatsConsumerConfig{ STORAGE_MODULE_DURABLE: {}, STORAGE_VELERO_INSTALL_DURABLE: {}, STORAGE_VELERO_POST_INSTALLATION_DURABLE: {}, + AUDIT_LOG_DURABLE: {}, } // getConsumerConfigMap will fetch the consumer wise config from the json string diff --git a/common-lib/pubsub-lib/audit/AuditLogEvent.go b/common-lib/pubsub-lib/audit/AuditLogEvent.go new file mode 100644 index 000000000..9295cddb1 --- /dev/null +++ b/common-lib/pubsub-lib/audit/AuditLogEvent.go @@ -0,0 +1,156 @@ +/* + * Copyright (c) 2024. Devtron Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package audit + +import ( + "encoding/json" + "time" +) + +// TimeFormat is the wire format for AuditLogEvent.Time (ISO-8601, numeric zone). +const TimeFormat = "2006-01-02T15:04:05-0700" + +// RouteName builds the MODULE:RESOURCE:ACTION route name read back by the +// orchestrator's audit log service. Using this helper keeps route tags +// consistent with the typed Module/Resource/Action constants instead of +// hand-written strings. +func RouteName(module AuditModule, resource AuditResource, action AuditAction) string { + return string(module) + ":" + string(resource) + ":" + string(action) +} + +// ResourceRef identifies the concrete resource an audited action touched. +type ResourceRef struct { + Type string `json:"type"` + Name string `json:"name,omitempty"` +} + +// EnrichmentEntity carries the raw identifier (e.g. an app_id) that the +// audit-log service resolves into the full entity during enrichment. +type EnrichmentEntity struct { + Identifier string `json:"identifier"` +} + +// AuditLogEvent is the canonical NATS payload published on AUDIT_LOG_TOPIC. +// It is shared between the publisher (orchestrator) and the consumer +// (audit-log service) so both agree on the contract. +type AuditLogEvent struct { + ApiPath string `json:"apiPath"` + Time string `json:"time"` + Resource ResourceRef `json:"resource"` + Module string `json:"module"` + Type string `json:"type"` // action verb, e.g. "create" + Action string `json:"action"` // human sentence, e.g. "Created application 'dashboard'" + UpdatedBy string `json:"updatedBy"` // acting user (email) who performed the action + ClientIp string `json:"clientIp"` // client IP the request originated from + RequestMethod string `json:"requestMethod"` // HTTP method of the audited request + ApiResponseCode int `json:"apiResponseCode"` // HTTP status code the request returned + ResponseTime time.Duration `json:"responseTime"` // request handling duration (nanoseconds) + Payload map[string]interface{} `json:"payload"` // the actual request body of the audited call, always present + EnrichmentContext map[string]EnrichmentEntity `json:"enrichmentContext,omitempty"` +} + +// NewAuditLogEvent constructs an event with the structural metadata parsed +// from a route name. eventTime is supplied by the caller (time.Now()) so this +// package stays free of implicit clock reads. +func NewAuditLogEvent(apiPath string, eventTime time.Time, module AuditModule, resourceType AuditResource, action AuditAction) *AuditLogEvent { + return &AuditLogEvent{ + ApiPath: apiPath, + Time: eventTime.Format(TimeFormat), + Module: module.ToString(), + Type: action.ToString(), + Resource: ResourceRef{Type: resourceType.ToString()}, + Payload: make(map[string]interface{}), + EnrichmentContext: make(map[string]EnrichmentEntity), + } +} + +// WithResourceName sets the human-facing name of the affected resource. +func (e *AuditLogEvent) WithResourceName(name string) *AuditLogEvent { + e.Resource.Name = name + return e +} + +// WithActor records the acting user (email) and originating client IP. The +// acting user is always available and is the "updated by" of the audited action. +func (e *AuditLogEvent) WithActor(updatedBy, clientIp string) *AuditLogEvent { + e.UpdatedBy = updatedBy + e.ClientIp = clientIp + return e +} + +// WithRequest records the request's HTTP method, response status code and +// handling duration. +func (e *AuditLogEvent) WithRequest(method string, apiResponseCode int, responseTime time.Duration) *AuditLogEvent { + e.RequestMethod = method + e.ApiResponseCode = apiResponseCode + e.ResponseTime = responseTime + return e +} + +// IsApiSuccess reports whether the audited request completed successfully (HTTP 2xx). +// Only successful actions are published to NATS — failed or denied requests +// (4xx/5xx) are not audited. +func (e *AuditLogEvent) IsApiSuccess() bool { + return e.ApiResponseCode >= 200 && e.ApiResponseCode < 300 +} + +// WithAction sets the human-readable action sentence. +func (e *AuditLogEvent) WithAction(action string) *AuditLogEvent { + e.Action = action + return e +} + +// WithPayloadField adds a single key/value to the payload bag. +func (e *AuditLogEvent) WithPayloadField(key string, value interface{}) *AuditLogEvent { + if e.Payload == nil { + e.Payload = make(map[string]interface{}) + } + e.Payload[key] = value + return e +} + +// WithEnrichment records an identifier (e.g. app_id) under an entity key +// (e.g. "app") for the consumer to resolve. Empty identifiers are ignored so +// callers can pass through missing path vars without polluting the context. +func (e *AuditLogEvent) WithEnrichment(entity, identifier string) *AuditLogEvent { + if identifier == "" { + return e + } + if e.EnrichmentContext == nil { + e.EnrichmentContext = make(map[string]EnrichmentEntity) + } + e.EnrichmentContext[entity] = EnrichmentEntity{Identifier: identifier} + return e +} + +// Marshal serializes the event to a JSON string for PubSubClientService.Publish. +func (e *AuditLogEvent) Marshal() (string, error) { + data, err := json.Marshal(e) + if err != nil { + return "", err + } + return string(data), nil +} + +// UnmarshalAuditLogEvent parses a NATS payload back into an event (consumer side). +func UnmarshalAuditLogEvent(data string) (*AuditLogEvent, error) { + event := &AuditLogEvent{} + if err := json.Unmarshal([]byte(data), event); err != nil { + return nil, err + } + return event, nil +} diff --git a/common-lib/pubsub-lib/audit/AuditLogEvent_test.go b/common-lib/pubsub-lib/audit/AuditLogEvent_test.go new file mode 100644 index 000000000..4d3ab9c23 --- /dev/null +++ b/common-lib/pubsub-lib/audit/AuditLogEvent_test.go @@ -0,0 +1,79 @@ +/* + * Copyright (c) 2024. Devtron Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package audit + +import ( + "encoding/json" + "testing" + "time" +) + +func TestAuditLogEvent_MarshalShape(t *testing.T) { + ts := time.Date(2026, 6, 29, 6, 7, 11, 0, time.UTC) + event := NewAuditLogEvent("/orchestrator/app/edit", ts, ModuleAppManagement, ResourceApplication, ActionCreate). + WithResourceName("dashboard"). + WithAction("Created application 'dashboard'"). + WithPayloadField("request_path", "/orchestrator/app/edit"). + WithPayloadField("http_method", "POST"). + WithEnrichment(EntityApp, "42"). + WithEnrichment(EntityUser, "7"). + WithEnrichment(EntityCluster, "") // empty identifier must be ignored + + out, err := event.Marshal() + if err != nil { + t.Fatalf("marshal: %v", err) + } + + // Round-trip into a generic map to assert the wire contract. + var m map[string]interface{} + if err := json.Unmarshal([]byte(out), &m); err != nil { + t.Fatalf("unmarshal: %v", err) + } + + if m["apiPath"] != "/orchestrator/app/edit" { + t.Errorf("apiPath = %v", m["apiPath"]) + } + if m["module"] != "application_management" { + t.Errorf("module = %v", m["module"]) + } + if m["type"] != "create" { + t.Errorf("type = %v", m["type"]) + } + if m["time"] != "2026-06-29T06:07:11+0000" { + t.Errorf("time = %v", m["time"]) + } + resource := m["resource"].(map[string]interface{}) + if resource["type"] != "application" || resource["name"] != "dashboard" { + t.Errorf("resource = %v", resource) + } + enrich := m["enrichmentContext"].(map[string]interface{}) + if _, ok := enrich[EntityCluster]; ok { + t.Errorf("empty-identifier entity must be dropped, got %v", enrich) + } + if enrich[EntityApp].(map[string]interface{})["identifier"] != "42" { + t.Errorf("app identifier = %v", enrich[EntityApp]) + } + + // Round-trip back to a typed event. + back, err := UnmarshalAuditLogEvent(out) + if err != nil { + t.Fatalf("unmarshal event: %v", err) + } + if back.Resource.Name != "dashboard" || back.EnrichmentContext[EntityUser].Identifier != "7" { + t.Errorf("round-trip mismatch: %+v", back) + } +} diff --git a/common-lib/pubsub-lib/audit/constants.go b/common-lib/pubsub-lib/audit/constants.go new file mode 100644 index 000000000..1b9b49a8d --- /dev/null +++ b/common-lib/pubsub-lib/audit/constants.go @@ -0,0 +1,148 @@ +/* + * Copyright (c) 2024. Devtron Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +// Package audit holds the shared audit-log contract: the structural +// MODULE / RESOURCE / ACTION vocabulary used to tag routes and the NATS +// message struct published by the orchestrator and consumed by the +// audit-log service. + +//go:generate go run github.com/devtron-labs/common-lib/tools/strenum -type=AuditModule +//go:generate go run github.com/devtron-labs/common-lib/tools/strenum -type=AuditResource +//go:generate go run github.com/devtron-labs/common-lib/tools/strenum -type=AuditAction +package audit + +// AuditModule is the top-level functional area an audited action belongs to. +// Values are stable, snake_case identifiers safe to persist and query on. +type AuditModule string + +func (m AuditModule) ToString() string { return string(m) } + +// Values are human-readable and surfaced directly on the UI, so they use spaces +// rather than underscores. +const ( + ModuleAppManagement AuditModule = "Application Management" + ModuleInfrastructureManagement AuditModule = "Infrastructure Management" + ModuleCostVisibility AuditModule = "Cost Visibility" + ModuleSoftwareDistributionHub AuditModule = "Software Distribution Hub" + ModuleGlobalConfiguration AuditModule = "Global Configuration" + ModuleUserManagement AuditModule = "User Management" +) + +// AuditResource is the kind of entity an audited action operates on. +// Seed the common ones here; extend as more routes are tagged. +type AuditResource string + +func (r AuditResource) ToString() string { return string(r) } + +// Values are human-readable and surfaced directly on the UI, so they use spaces +// rather than underscores. +const ( + ResourceApplication AuditResource = "Application" + ResourceHelmApp AuditResource = "Helm App" + ResourceJob AuditResource = "Job" + ResourceEnvironment AuditResource = "Environment" + ResourceCluster AuditResource = "Cluster" + ResourceCdPipeline AuditResource = "Cd Pipeline" + ResourceCiPipeline AuditResource = "Ci Pipeline" + ResourceBuildConfig AuditResource = "Build Config" + ResourceWorkflow AuditResource = "Workflow" + ResourceDeploymentTemplate AuditResource = "Deployment Template" + ResourceConfigMap AuditResource = "ConfigMap" + ResourceSecret AuditResource = "Secret" + ResourceGitMaterial AuditResource = "Git Material" + ResourcePod AuditResource = "Pod" + ResourceUser AuditResource = "User" + ResourcePermissionGroup AuditResource = "Permission Group" + ResourceUserGroup AuditResource = "User Group" + ResourceApiToken AuditResource = "Api Token" + ResourceTeam AuditResource = "Project" // "team" is the internal name; the product/UI calls it a Project + // global configuration resources + ResourceGitOpsConfig AuditResource = "Gitops Config" + ResourceDockerRegistry AuditResource = "Docker Registry" + ResourceGitProvider AuditResource = "Git Provider" + ResourceGitHost AuditResource = "Git Host" + ResourceChartRepo AuditResource = "Chart Repo" + ResourceSSOConfig AuditResource = "Sso Config" + ResourceNotification AuditResource = "Notification Config" + ResourceAuthorization AuditResource = "Authorization Config" + // infrastructure management resources (resource browser) + ResourceK8sResource AuditResource = "Kubernetes Resource" + ResourceNode AuditResource = "Node" + // application management resources + ResourceDeploymentWindowProfile AuditResource = "Deployment Window Profile" + // security & release-gating resources + ResourceVulnerabilityPolicy AuditResource = "Vulnerability Policy" + ResourceArtifactPromotionPolicy AuditResource = "Artifact Promotion Policy" + ResourceImageApproval AuditResource = "Image Approval" + // additional global-configuration resources + ResourceCustomChart AuditResource = "Custom Chart" + ResourceChartGroup AuditResource = "Chart Group" + ResourceBuildInfraProfile AuditResource = "Build Infra Profile" + ResourceClusterCategory AuditResource = "Cluster Category" + ResourceHostUrl AuditResource = "Host Url" + ResourceScopedVariable AuditResource = "Scoped Variable" + ResourceGlobalTag AuditResource = "Global Tag" + // enterprise (protect / policy) resources + ResourceLockConfiguration AuditResource = "Lock Configuration" + ResourceLockConfigurationAssignment AuditResource = "Lock Configuration Assignment" + ResourceConfigDraft AuditResource = "Config Draft" + ResourceFilterCondition AuditResource = "Filter Condition" + ResourcePullImageDigestPolicy AuditResource = "Pull Image Digest Policy" + ResourcePlugin AuditResource = "Plugin" + // cluster terminal pod + ResourceClusterTerminal AuditResource = "Cluster Terminal" + // resource watcher + ResourceWatcher AuditResource = "Resource Watcher" + // external link + ResourceExternalLink AuditResource = "External Link" +) + +// AuditAction is the verb performed on the resource. It populates the +// AuditLogEvent.Type field; the human-readable sentence goes in Action. +type AuditAction string + +func (a AuditAction) ToString() string { return string(a) } + +const ( + ActionCreate AuditAction = "Create" + ActionUpdate AuditAction = "Update" + ActionDelete AuditAction = "Delete" + ActionTrigger AuditAction = "Trigger" + ActionDeploy AuditAction = "Deploy" + ActionHibernate AuditAction = "Hibernate" + ActionUnHibernate AuditAction = "Unhibernate" + ActionRollback AuditAction = "Rollback" + ActionSync AuditAction = "Sync" + ActionApprove AuditAction = "Approve" + ActionClone AuditAction = "Clone" + ActionAssign AuditAction = "Assign" + ActionGet AuditAction = "Get" + ActionExec AuditAction = "Exec" + ActionApply AuditAction = "Apply" +) + +// Enrichment entity keys used in AuditLogEvent.EnrichmentContext. These name +// the entity whose identifier the audit-log service resolves into a full +// record (e.g. "app" -> app_id -> application name/team/etc). +const ( + EntityApp = "App" + EntityUser = "User" + EntityEnvironment = "Environment" + EntityCluster = "Cluster" + EntityTeam = "Team" + EntityPipeline = "Pipeline" + EntityAppWorkflow = "App Workflow" +) diff --git a/common-lib/pubsub-lib/audit/constants_auditaction_gen.go b/common-lib/pubsub-lib/audit/constants_auditaction_gen.go new file mode 100644 index 000000000..74b7410a3 --- /dev/null +++ b/common-lib/pubsub-lib/audit/constants_auditaction_gen.go @@ -0,0 +1,48 @@ +// Code generated by strenum; DO NOT EDIT. + +package audit + +var _AllAuditAction = []AuditAction{ + ActionCreate, + ActionUpdate, + ActionDelete, + ActionTrigger, + ActionDeploy, + ActionHibernate, + ActionUnHibernate, + ActionRollback, + ActionSync, + ActionApprove, + ActionClone, + ActionAssign, + ActionGet, + ActionExec, + ActionApply, +} + +var _AuditActionMap = map[AuditAction]struct{}{ + ActionCreate: {}, + ActionUpdate: {}, + ActionDelete: {}, + ActionTrigger: {}, + ActionDeploy: {}, + ActionHibernate: {}, + ActionUnHibernate: {}, + ActionRollback: {}, + ActionSync: {}, + ActionApprove: {}, + ActionClone: {}, + ActionAssign: {}, + ActionGet: {}, + ActionExec: {}, + ActionApply: {}, +} + +func AuditActionValues() []AuditAction { + return _AllAuditAction +} + +func (x AuditAction) IsValid() bool { + _, ok := _AuditActionMap[x] + return ok +} diff --git a/common-lib/pubsub-lib/audit/constants_auditmodule_gen.go b/common-lib/pubsub-lib/audit/constants_auditmodule_gen.go new file mode 100644 index 000000000..737225863 --- /dev/null +++ b/common-lib/pubsub-lib/audit/constants_auditmodule_gen.go @@ -0,0 +1,30 @@ +// Code generated by strenum; DO NOT EDIT. + +package audit + +var _AllAuditModule = []AuditModule{ + ModuleAppManagement, + ModuleInfrastructureManagement, + ModuleCostVisibility, + ModuleSoftwareDistributionHub, + ModuleGlobalConfiguration, + ModuleUserManagement, +} + +var _AuditModuleMap = map[AuditModule]struct{}{ + ModuleAppManagement: {}, + ModuleInfrastructureManagement: {}, + ModuleCostVisibility: {}, + ModuleSoftwareDistributionHub: {}, + ModuleGlobalConfiguration: {}, + ModuleUserManagement: {}, +} + +func AuditModuleValues() []AuditModule { + return _AllAuditModule +} + +func (x AuditModule) IsValid() bool { + _, ok := _AuditModuleMap[x] + return ok +} diff --git a/common-lib/pubsub-lib/audit/constants_auditresource_gen.go b/common-lib/pubsub-lib/audit/constants_auditresource_gen.go new file mode 100644 index 000000000..c3b136df2 --- /dev/null +++ b/common-lib/pubsub-lib/audit/constants_auditresource_gen.go @@ -0,0 +1,116 @@ +// Code generated by strenum; DO NOT EDIT. + +package audit + +var _AllAuditResource = []AuditResource{ + ResourceApplication, + ResourceHelmApp, + ResourceJob, + ResourceEnvironment, + ResourceCluster, + ResourceCdPipeline, + ResourceCiPipeline, + ResourceBuildConfig, + ResourceWorkflow, + ResourceDeploymentTemplate, + ResourceConfigMap, + ResourceSecret, + ResourceGitMaterial, + ResourcePod, + ResourceUser, + ResourcePermissionGroup, + ResourceUserGroup, + ResourceApiToken, + ResourceTeam, + ResourceGitOpsConfig, + ResourceDockerRegistry, + ResourceGitProvider, + ResourceGitHost, + ResourceChartRepo, + ResourceSSOConfig, + ResourceNotification, + ResourceAuthorization, + ResourceK8sResource, + ResourceNode, + ResourceDeploymentWindowProfile, + ResourceVulnerabilityPolicy, + ResourceArtifactPromotionPolicy, + ResourceImageApproval, + ResourceCustomChart, + ResourceChartGroup, + ResourceBuildInfraProfile, + ResourceClusterCategory, + ResourceHostUrl, + ResourceScopedVariable, + ResourceGlobalTag, + ResourceLockConfiguration, + ResourceLockConfigurationAssignment, + ResourceConfigDraft, + ResourceFilterCondition, + ResourcePullImageDigestPolicy, + ResourcePlugin, + ResourceClusterTerminal, + ResourceWatcher, + ResourceExternalLink, +} + +var _AuditResourceMap = map[AuditResource]struct{}{ + ResourceApplication: {}, + ResourceHelmApp: {}, + ResourceJob: {}, + ResourceEnvironment: {}, + ResourceCluster: {}, + ResourceCdPipeline: {}, + ResourceCiPipeline: {}, + ResourceBuildConfig: {}, + ResourceWorkflow: {}, + ResourceDeploymentTemplate: {}, + ResourceConfigMap: {}, + ResourceSecret: {}, + ResourceGitMaterial: {}, + ResourcePod: {}, + ResourceUser: {}, + ResourcePermissionGroup: {}, + ResourceUserGroup: {}, + ResourceApiToken: {}, + ResourceTeam: {}, + ResourceGitOpsConfig: {}, + ResourceDockerRegistry: {}, + ResourceGitProvider: {}, + ResourceGitHost: {}, + ResourceChartRepo: {}, + ResourceSSOConfig: {}, + ResourceNotification: {}, + ResourceAuthorization: {}, + ResourceK8sResource: {}, + ResourceNode: {}, + ResourceDeploymentWindowProfile: {}, + ResourceVulnerabilityPolicy: {}, + ResourceArtifactPromotionPolicy: {}, + ResourceImageApproval: {}, + ResourceCustomChart: {}, + ResourceChartGroup: {}, + ResourceBuildInfraProfile: {}, + ResourceClusterCategory: {}, + ResourceHostUrl: {}, + ResourceScopedVariable: {}, + ResourceGlobalTag: {}, + ResourceLockConfiguration: {}, + ResourceLockConfigurationAssignment: {}, + ResourceConfigDraft: {}, + ResourceFilterCondition: {}, + ResourcePullImageDigestPolicy: {}, + ResourcePlugin: {}, + ResourceClusterTerminal: {}, + ResourceWatcher: {}, + ResourceExternalLink: {}, +} + +func AuditResourceValues() []AuditResource { + return _AllAuditResource +} + +func (x AuditResource) IsValid() bool { + _, ok := _AuditResourceMap[x] + return ok +} diff --git a/common-lib/tools/strenum/strenum.go b/common-lib/tools/strenum/strenum.go new file mode 100644 index 000000000..0966d4726 --- /dev/null +++ b/common-lib/tools/strenum/strenum.go @@ -0,0 +1,111 @@ +package main + +import ( + "bytes" + "flag" + "fmt" + "go/ast" + "go/parser" + "go/token" + "os" + "path/filepath" + "strings" +) + +// logErr outputs errors to Stderr to keep standard output clean +func logErr(format string, a ...interface{}) { + fmt.Fprintf(os.Stderr, "Error: "+format+"\n", a...) +} + +func main() { + typeName := flag.String("type", "", "name of the type to generate collections for") + flag.Parse() + + if *typeName == "" { + logErr("-type flag is required") + os.Exit(1) + } + + gofile := os.Getenv("GOFILE") + if gofile == "" { + logErr("GOFILE environment variable not set. Run via 'go generate'") + os.Exit(1) + } + + fset := token.NewFileSet() + node, err := parser.ParseFile(fset, gofile, nil, parser.ParseComments) + if err != nil { + logErr("Failed to parse file %s: %v", gofile, err) + os.Exit(1) + } + + var constants []string + ast.Inspect(node, func(n ast.Node) bool { + decl, ok := n.(*ast.GenDecl) + if !ok || decl.Tok != token.CONST { + return true + } + + for _, spec := range decl.Specs { + vSpec, ok := spec.(*ast.ValueSpec) + if !ok { + continue + } + + // Check if the constant explicitly matches our target type + isTargetType := false + if vSpec.Type != nil { + if ident, ok := vSpec.Type.(*ast.Ident); ok && ident.Name == *typeName { + isTargetType = true + } + } + + if isTargetType { + for _, name := range vSpec.Names { + constants = append(constants, name.Name) + } + } + } + return true + }) + + if len(constants) == 0 { + logErr("No constants found for type %s in file %s", *typeName, gofile) + os.Exit(0) + } + + // Generate the companion code + var buf bytes.Buffer + buf.WriteString(fmt.Sprintf("// Code generated by strenum; DO NOT EDIT.\n\npackage %s\n\n", node.Name.Name)) + + // 1. Generate the Slice + buf.WriteString(fmt.Sprintf("var _All%s = []%s{\n", *typeName, *typeName)) + for _, c := range constants { + buf.WriteString(fmt.Sprintf("\t%s,\n", c)) + } + buf.WriteString("}\n\n") + + // 2. Generate the Map + buf.WriteString(fmt.Sprintf("var _%sMap = map[%s]struct{}{\n", *typeName, *typeName)) + for _, c := range constants { + buf.WriteString(fmt.Sprintf("\t%s: {},\n", c)) + } + buf.WriteString("}\n\n") + + // 3. Generate Helper Methods + buf.WriteString(fmt.Sprintf("func %sValues() []%s {\n\treturn _All%s\n}\n\n", *typeName, *typeName, *typeName)) + buf.WriteString(fmt.Sprintf("func (x %s) IsValid() bool {\n\t_, ok := _%sMap[x]\n\treturn ok\n}\n", *typeName, *typeName)) + + ext := filepath.Ext(gofile) + base := strings.TrimSuffix(gofile, ext) + outputFile := fmt.Sprintf("%s_%s_gen.go", base, strings.ToLower(*typeName)) + + err = os.WriteFile(outputFile, buf.Bytes(), 0644) + if err != nil { + logErr("Failed to write output file %s: %v", outputFile, err) + os.Exit(1) + } + + // Success feedback + fmt.Printf("Generated: %s (%d constants found)\n", outputFile, len(constants)) +}