diff --git a/api/context.go b/api/context.go index b401c3f42..6bbd96bb3 100644 --- a/api/context.go +++ b/api/context.go @@ -9,7 +9,6 @@ import ( "github.com/flanksource/duty" "github.com/flanksource/duty/models" "github.com/flanksource/duty/types" - "github.com/flanksource/kommons" "github.com/google/uuid" "github.com/jackc/pgx/v5/pgxpool" "github.com/labstack/echo/v4" @@ -39,7 +38,6 @@ type Context interface { Namespace() string Kubernetes() kubernetes.Interface - Kommons() *kommons.Client WithDB(db *gorm.DB) Context WithEchoContext(ctx EchoContext) Context @@ -70,7 +68,6 @@ type context struct { db *gorm.DB pool *pgxpool.Pool - kommons *kommons.Client kubernetes kubernetes.Interface namespace string } @@ -81,17 +78,12 @@ func NewContext(db *gorm.DB, pool *pgxpool.Pool) Context { db: db, pool: pool, kubernetes: Kubernetes, - kommons: KommonsClient, namespace: Namespace, } return c } -func (c *context) Kommons() *kommons.Client { - return c.kommons -} - func (c *context) Kubernetes() kubernetes.Interface { return c.kubernetes } diff --git a/api/global.go b/api/global.go index 8ffc05002..7030fab4f 100644 --- a/api/global.go +++ b/api/global.go @@ -1,7 +1,6 @@ package api import ( - "github.com/flanksource/kommons" "github.com/google/uuid" "k8s.io/client-go/kubernetes" ) @@ -13,7 +12,6 @@ var ( CanaryCheckerPath string ApmHubPath string Kubernetes kubernetes.Interface - KommonsClient *kommons.Client Namespace string // Full URL of the mission control web UI. diff --git a/cmd/root.go b/cmd/root.go index 50c95a856..fe435a57f 100644 --- a/cmd/root.go +++ b/cmd/root.go @@ -7,6 +7,7 @@ import ( "time" "github.com/flanksource/commons/logger" + "github.com/flanksource/commons/utils" "github.com/flanksource/incident-commander/api" "github.com/flanksource/incident-commander/db" "github.com/flanksource/incident-commander/jobs" @@ -31,11 +32,6 @@ func PreRun(cmd *cobra.Command, args []string) { api.Kubernetes = fake.NewSimpleClientset() } - api.KommonsClient, _, err = k8s.NewKommonsClient() - if err != nil { - logger.Warnf("Failed to get kubernetes client: %v", err) - } - api.DefaultContext = api.NewContext(db.Gorm, db.Pool) if otelcollectorURL != "" { @@ -59,7 +55,7 @@ var otelcollectorURL string func ServerFlags(flags *pflag.FlagSet) { flags.IntVar(&httpPort, "httpPort", 8080, "Port to expose a health dashboard") - flags.StringVar(&api.Namespace, "namespace", os.Getenv("NAMESPACE"), "Namespace to use for config/secret lookups") + flags.StringVar(&api.Namespace, "namespace", utils.Coalesce(os.Getenv("NAMESPACE"), "default"), "Namespace to use for config/secret lookups") flags.IntVar(&devGuiPort, "devGuiPort", 3004, "Port used by a local npm server in development mode") flags.IntVar(&metricsPort, "metricsPort", 8081, "Port to expose a health dashboard ") flags.BoolVar(&dev, "dev", false, "Run in development mode") diff --git a/config/crds/mission-control.flanksource.com_playbooks.yaml b/config/crds/mission-control.flanksource.com_playbooks.yaml index 3a84de21a..05328b5e6 100644 --- a/config/crds/mission-control.flanksource.com_playbooks.yaml +++ b/config/crds/mission-control.flanksource.com_playbooks.yaml @@ -299,10 +299,6 @@ spec: type: object type: object type: array - maxSSLExpiry: - description: Maximum number of days until the SSL Certificate - expires. - type: integer method: description: Method to use - defaults to GET type: string @@ -346,10 +342,6 @@ spec: description: TemplateBody controls whether the body of the request needs to be templated type: boolean - thresholdMillis: - description: Maximum duration in milliseconds for the HTTP - request. It will fail the check if it takes longer. - type: integer url: description: Connection url, interpolated with username,password type: string @@ -386,6 +378,10 @@ spec: type: string pod: properties: + maxLength: + description: 'MaxLength is the maximum length of the logs + to show Default: 3000 characters' + type: integer name: description: Name is name of the pod that'll be created type: string @@ -406,9 +402,6 @@ spec: connection: description: Connection identifier e.g. connection://Postgres/flanksource type: string - database: - description: Database is the name of the database - type: string driver: description: 'Driver is the name of the underlying database to connect to. Example: postgres, mysql, ...' @@ -420,7 +413,6 @@ spec: description: URL is the database connection url type: string required: - - database - driver - query type: object diff --git a/config/schemas/playbook.schema.json b/config/schemas/playbook.schema.json index 606a62cd0..365cace6d 100644 --- a/config/schemas/playbook.schema.json +++ b/config/schemas/playbook.schema.json @@ -1 +1 @@ -{"$schema":"http://json-schema.org/draft-04/schema#","$ref":"#/definitions/Playbook","definitions":{"AWSConnection":{"properties":{"connection":{"type":"string"},"accessKey":{"$schema":"http://json-schema.org/draft-04/schema#","$ref":"#/definitions/EnvVar"},"secretKey":{"$ref":"#/definitions/EnvVar"},"sessionToken":{"$ref":"#/definitions/EnvVar"},"region":{"type":"string"},"endpoint":{"type":"string"},"skipTLSVerify":{"type":"boolean"},"objectPath":{"type":"string"},"usePathStyle":{"type":"boolean"}},"additionalProperties":false,"type":"object"},"AzureConnection":{"properties":{"connection":{"type":"string"},"clientID":{"$ref":"#/definitions/EnvVar"},"clientSecret":{"$ref":"#/definitions/EnvVar"},"tenantID":{"type":"string"}},"additionalProperties":false,"type":"object"},"ConfigMapKeySelector":{"required":["key"],"properties":{"name":{"type":"string"},"key":{"type":"string"}},"additionalProperties":false,"type":"object"},"EnvVar":{"properties":{"name":{"type":"string"},"value":{"type":"string"},"valueFrom":{"$schema":"http://json-schema.org/draft-04/schema#","$ref":"#/definitions/EnvVarSource"}},"additionalProperties":false,"type":"object"},"EnvVarSource":{"properties":{"configMapKeyRef":{"$schema":"http://json-schema.org/draft-04/schema#","$ref":"#/definitions/ConfigMapKeySelector"},"secretKeyRef":{"$schema":"http://json-schema.org/draft-04/schema#","$ref":"#/definitions/SecretKeySelector"}},"additionalProperties":false,"type":"object"},"ExecAction":{"required":["script"],"properties":{"script":{"type":"string"},"connections":{"$schema":"http://json-schema.org/draft-04/schema#","$ref":"#/definitions/ExecConnections"}},"additionalProperties":false,"type":"object"},"ExecConnections":{"properties":{"aws":{"$schema":"http://json-schema.org/draft-04/schema#","$ref":"#/definitions/AWSConnection"},"gcp":{"$schema":"http://json-schema.org/draft-04/schema#","$ref":"#/definitions/GCPConnection"},"azure":{"$schema":"http://json-schema.org/draft-04/schema#","$ref":"#/definitions/AzureConnection"}},"additionalProperties":false,"type":"object"},"FieldsV1":{"properties":{},"additionalProperties":false,"type":"object"},"GCPConnection":{"properties":{"connection":{"type":"string"},"endpoint":{"type":"string"},"credentials":{"$ref":"#/definitions/EnvVar"}},"additionalProperties":false,"type":"object"},"HTTPAction":{"properties":{"connection":{"type":"string"},"url":{"type":"string"},"username":{"$ref":"#/definitions/EnvVar"},"password":{"$ref":"#/definitions/EnvVar"},"thresholdMillis":{"type":"integer"},"maxSSLExpiry":{"type":"integer"},"method":{"type":"string"},"ntlm":{"type":"boolean"},"ntlmv2":{"type":"boolean"},"headers":{"items":{"$ref":"#/definitions/EnvVar"},"type":"array"},"body":{"type":"string"},"templateBody":{"type":"boolean"}},"additionalProperties":false,"type":"object"},"ManagedFieldsEntry":{"properties":{"manager":{"type":"string"},"operation":{"type":"string"},"apiVersion":{"type":"string"},"time":{"$ref":"#/definitions/Time"},"fieldsType":{"type":"string"},"fieldsV1":{"$schema":"http://json-schema.org/draft-04/schema#","$ref":"#/definitions/FieldsV1"},"subresource":{"type":"string"}},"additionalProperties":false,"type":"object"},"ObjectMeta":{"properties":{"name":{"type":"string"},"generateName":{"type":"string"},"namespace":{"type":"string"},"selfLink":{"type":"string"},"uid":{"type":"string"},"resourceVersion":{"type":"string"},"generation":{"type":"integer"},"creationTimestamp":{"$schema":"http://json-schema.org/draft-04/schema#","$ref":"#/definitions/Time"},"deletionTimestamp":{"$ref":"#/definitions/Time"},"deletionGracePeriodSeconds":{"type":"integer"},"labels":{"patternProperties":{".*":{"type":"string"}},"type":"object"},"annotations":{"patternProperties":{".*":{"type":"string"}},"type":"object"},"ownerReferences":{"items":{"$schema":"http://json-schema.org/draft-04/schema#","$ref":"#/definitions/OwnerReference"},"type":"array"},"finalizers":{"items":{"type":"string"},"type":"array"},"managedFields":{"items":{"$schema":"http://json-schema.org/draft-04/schema#","$ref":"#/definitions/ManagedFieldsEntry"},"type":"array"}},"additionalProperties":false,"type":"object"},"OwnerReference":{"required":["apiVersion","kind","name","uid"],"properties":{"apiVersion":{"type":"string"},"kind":{"type":"string"},"name":{"type":"string"},"uid":{"type":"string"},"controller":{"type":"boolean"},"blockOwnerDeletion":{"type":"boolean"}},"additionalProperties":false,"type":"object"},"Permission":{"properties":{"role":{"type":"string"},"team":{"type":"string"},"ref":{"type":"string"}},"additionalProperties":false,"type":"object"},"Playbook":{"properties":{"kind":{"type":"string"},"apiVersion":{"type":"string"},"metadata":{"$schema":"http://json-schema.org/draft-04/schema#","$ref":"#/definitions/ObjectMeta"},"spec":{"$schema":"http://json-schema.org/draft-04/schema#","$ref":"#/definitions/PlaybookSpec"},"status":{"$schema":"http://json-schema.org/draft-04/schema#","$ref":"#/definitions/PlaybookStatus"}},"additionalProperties":false,"type":"object"},"PlaybookAction":{"required":["name"],"properties":{"name":{"type":"string"},"exec":{"$schema":"http://json-schema.org/draft-04/schema#","$ref":"#/definitions/ExecAction"},"http":{"$schema":"http://json-schema.org/draft-04/schema#","$ref":"#/definitions/HTTPAction"},"sql":{"$schema":"http://json-schema.org/draft-04/schema#","$ref":"#/definitions/SQLAction"},"pod":{"$schema":"http://json-schema.org/draft-04/schema#","$ref":"#/definitions/PodAction"}},"additionalProperties":false,"type":"object"},"PlaybookApproval":{"properties":{"type":{"type":"string"},"approvers":{"$schema":"http://json-schema.org/draft-04/schema#","$ref":"#/definitions/PlaybookApprovers"}},"additionalProperties":false,"type":"object"},"PlaybookApprovers":{"properties":{"people":{"items":{"type":"string"},"type":"array"},"teams":{"items":{"type":"string"},"type":"array"}},"additionalProperties":false,"type":"object"},"PlaybookEvent":{"properties":{"canary":{"items":{"$schema":"http://json-schema.org/draft-04/schema#","$ref":"#/definitions/PlaybookEventDetail"},"type":"array"},"component":{"items":{"$ref":"#/definitions/PlaybookEventDetail"},"type":"array"}},"additionalProperties":false,"type":"object"},"PlaybookEventDetail":{"required":["event"],"properties":{"labels":{"patternProperties":{".*":{"type":"string"}},"type":"object"},"filter":{"type":"string"},"event":{"type":"string"}},"additionalProperties":false,"type":"object"},"PlaybookParameter":{"required":["name","label"],"properties":{"name":{"type":"string"},"label":{"type":"string"}},"additionalProperties":false,"type":"object"},"PlaybookResourceFilter":{"properties":{"type":{"type":"string"},"tags":{"patternProperties":{".*":{"type":"string"}},"type":"object"}},"additionalProperties":false,"type":"object"},"PlaybookSpec":{"required":["actions"],"properties":{"description":{"type":"string"},"on":{"$schema":"http://json-schema.org/draft-04/schema#","$ref":"#/definitions/PlaybookEvent"},"permissions":{"items":{"$schema":"http://json-schema.org/draft-04/schema#","$ref":"#/definitions/Permission"},"type":"array"},"configs":{"items":{"$schema":"http://json-schema.org/draft-04/schema#","$ref":"#/definitions/PlaybookResourceFilter"},"type":"array"},"checks":{"items":{"$ref":"#/definitions/PlaybookResourceFilter"},"type":"array"},"components":{"items":{"$ref":"#/definitions/PlaybookResourceFilter"},"type":"array"},"parameters":{"items":{"$schema":"http://json-schema.org/draft-04/schema#","$ref":"#/definitions/PlaybookParameter"},"type":"array"},"actions":{"items":{"$schema":"http://json-schema.org/draft-04/schema#","$ref":"#/definitions/PlaybookAction"},"type":"array"},"approval":{"$schema":"http://json-schema.org/draft-04/schema#","$ref":"#/definitions/PlaybookApproval"}},"additionalProperties":false,"type":"object"},"PlaybookStatus":{"properties":{},"additionalProperties":false,"type":"object"},"PodAction":{"required":["name","spec"],"properties":{"name":{"type":"string"},"timeout":{"type":"integer"},"spec":{"additionalProperties":true}},"additionalProperties":false,"type":"object"},"SQLAction":{"required":["database","query","driver"],"properties":{"connection":{"type":"string"},"url":{"type":"string"},"database":{"type":"string"},"query":{"type":"string"},"driver":{"type":"string"}},"additionalProperties":false,"type":"object"},"SecretKeySelector":{"required":["key"],"properties":{"name":{"type":"string"},"key":{"type":"string"}},"additionalProperties":false,"type":"object"},"Time":{"properties":{},"additionalProperties":false,"type":"object"}}} \ No newline at end of file +{"$schema":"http://json-schema.org/draft-04/schema#","$ref":"#/definitions/Playbook","definitions":{"AWSConnection":{"properties":{"connection":{"type":"string"},"accessKey":{"$schema":"http://json-schema.org/draft-04/schema#","$ref":"#/definitions/EnvVar"},"secretKey":{"$ref":"#/definitions/EnvVar"},"sessionToken":{"$ref":"#/definitions/EnvVar"},"region":{"type":"string"},"endpoint":{"type":"string"},"skipTLSVerify":{"type":"boolean"},"objectPath":{"type":"string"},"usePathStyle":{"type":"boolean"}},"additionalProperties":false,"type":"object"},"AzureConnection":{"properties":{"connection":{"type":"string"},"clientID":{"$ref":"#/definitions/EnvVar"},"clientSecret":{"$ref":"#/definitions/EnvVar"},"tenantID":{"type":"string"}},"additionalProperties":false,"type":"object"},"ConfigMapKeySelector":{"required":["key"],"properties":{"name":{"type":"string"},"key":{"type":"string"}},"additionalProperties":false,"type":"object"},"EnvVar":{"properties":{"name":{"type":"string"},"value":{"type":"string"},"valueFrom":{"$schema":"http://json-schema.org/draft-04/schema#","$ref":"#/definitions/EnvVarSource"}},"additionalProperties":false,"type":"object"},"EnvVarSource":{"properties":{"configMapKeyRef":{"$schema":"http://json-schema.org/draft-04/schema#","$ref":"#/definitions/ConfigMapKeySelector"},"secretKeyRef":{"$schema":"http://json-schema.org/draft-04/schema#","$ref":"#/definitions/SecretKeySelector"}},"additionalProperties":false,"type":"object"},"ExecAction":{"required":["script"],"properties":{"script":{"type":"string"},"connections":{"$schema":"http://json-schema.org/draft-04/schema#","$ref":"#/definitions/ExecConnections"}},"additionalProperties":false,"type":"object"},"ExecConnections":{"properties":{"aws":{"$schema":"http://json-schema.org/draft-04/schema#","$ref":"#/definitions/AWSConnection"},"gcp":{"$schema":"http://json-schema.org/draft-04/schema#","$ref":"#/definitions/GCPConnection"},"azure":{"$schema":"http://json-schema.org/draft-04/schema#","$ref":"#/definitions/AzureConnection"}},"additionalProperties":false,"type":"object"},"FieldsV1":{"properties":{},"additionalProperties":false,"type":"object"},"GCPConnection":{"properties":{"connection":{"type":"string"},"endpoint":{"type":"string"},"credentials":{"$ref":"#/definitions/EnvVar"}},"additionalProperties":false,"type":"object"},"HTTPAction":{"properties":{"connection":{"type":"string"},"url":{"type":"string"},"username":{"$ref":"#/definitions/EnvVar"},"password":{"$ref":"#/definitions/EnvVar"},"method":{"type":"string"},"ntlm":{"type":"boolean"},"ntlmv2":{"type":"boolean"},"headers":{"items":{"$ref":"#/definitions/EnvVar"},"type":"array"},"body":{"type":"string"},"templateBody":{"type":"boolean"}},"additionalProperties":false,"type":"object"},"ManagedFieldsEntry":{"properties":{"manager":{"type":"string"},"operation":{"type":"string"},"apiVersion":{"type":"string"},"time":{"$ref":"#/definitions/Time"},"fieldsType":{"type":"string"},"fieldsV1":{"$schema":"http://json-schema.org/draft-04/schema#","$ref":"#/definitions/FieldsV1"},"subresource":{"type":"string"}},"additionalProperties":false,"type":"object"},"ObjectMeta":{"properties":{"name":{"type":"string"},"generateName":{"type":"string"},"namespace":{"type":"string"},"selfLink":{"type":"string"},"uid":{"type":"string"},"resourceVersion":{"type":"string"},"generation":{"type":"integer"},"creationTimestamp":{"$schema":"http://json-schema.org/draft-04/schema#","$ref":"#/definitions/Time"},"deletionTimestamp":{"$ref":"#/definitions/Time"},"deletionGracePeriodSeconds":{"type":"integer"},"labels":{"patternProperties":{".*":{"type":"string"}},"type":"object"},"annotations":{"patternProperties":{".*":{"type":"string"}},"type":"object"},"ownerReferences":{"items":{"$schema":"http://json-schema.org/draft-04/schema#","$ref":"#/definitions/OwnerReference"},"type":"array"},"finalizers":{"items":{"type":"string"},"type":"array"},"managedFields":{"items":{"$schema":"http://json-schema.org/draft-04/schema#","$ref":"#/definitions/ManagedFieldsEntry"},"type":"array"}},"additionalProperties":false,"type":"object"},"OwnerReference":{"required":["apiVersion","kind","name","uid"],"properties":{"apiVersion":{"type":"string"},"kind":{"type":"string"},"name":{"type":"string"},"uid":{"type":"string"},"controller":{"type":"boolean"},"blockOwnerDeletion":{"type":"boolean"}},"additionalProperties":false,"type":"object"},"Permission":{"properties":{"role":{"type":"string"},"team":{"type":"string"},"ref":{"type":"string"}},"additionalProperties":false,"type":"object"},"Playbook":{"properties":{"kind":{"type":"string"},"apiVersion":{"type":"string"},"metadata":{"$schema":"http://json-schema.org/draft-04/schema#","$ref":"#/definitions/ObjectMeta"},"spec":{"$schema":"http://json-schema.org/draft-04/schema#","$ref":"#/definitions/PlaybookSpec"},"status":{"$schema":"http://json-schema.org/draft-04/schema#","$ref":"#/definitions/PlaybookStatus"}},"additionalProperties":false,"type":"object"},"PlaybookAction":{"required":["name"],"properties":{"name":{"type":"string"},"exec":{"$schema":"http://json-schema.org/draft-04/schema#","$ref":"#/definitions/ExecAction"},"http":{"$schema":"http://json-schema.org/draft-04/schema#","$ref":"#/definitions/HTTPAction"},"sql":{"$schema":"http://json-schema.org/draft-04/schema#","$ref":"#/definitions/SQLAction"},"pod":{"$schema":"http://json-schema.org/draft-04/schema#","$ref":"#/definitions/PodAction"}},"additionalProperties":false,"type":"object"},"PlaybookApproval":{"properties":{"type":{"type":"string"},"approvers":{"$schema":"http://json-schema.org/draft-04/schema#","$ref":"#/definitions/PlaybookApprovers"}},"additionalProperties":false,"type":"object"},"PlaybookApprovers":{"properties":{"people":{"items":{"type":"string"},"type":"array"},"teams":{"items":{"type":"string"},"type":"array"}},"additionalProperties":false,"type":"object"},"PlaybookEvent":{"properties":{"canary":{"items":{"$schema":"http://json-schema.org/draft-04/schema#","$ref":"#/definitions/PlaybookEventDetail"},"type":"array"},"component":{"items":{"$ref":"#/definitions/PlaybookEventDetail"},"type":"array"}},"additionalProperties":false,"type":"object"},"PlaybookEventDetail":{"required":["event"],"properties":{"labels":{"patternProperties":{".*":{"type":"string"}},"type":"object"},"filter":{"type":"string"},"event":{"type":"string"}},"additionalProperties":false,"type":"object"},"PlaybookParameter":{"required":["name","label"],"properties":{"name":{"type":"string"},"label":{"type":"string"}},"additionalProperties":false,"type":"object"},"PlaybookResourceFilter":{"properties":{"type":{"type":"string"},"tags":{"patternProperties":{".*":{"type":"string"}},"type":"object"}},"additionalProperties":false,"type":"object"},"PlaybookSpec":{"required":["actions"],"properties":{"description":{"type":"string"},"on":{"$schema":"http://json-schema.org/draft-04/schema#","$ref":"#/definitions/PlaybookEvent"},"permissions":{"items":{"$schema":"http://json-schema.org/draft-04/schema#","$ref":"#/definitions/Permission"},"type":"array"},"configs":{"items":{"$schema":"http://json-schema.org/draft-04/schema#","$ref":"#/definitions/PlaybookResourceFilter"},"type":"array"},"checks":{"items":{"$ref":"#/definitions/PlaybookResourceFilter"},"type":"array"},"components":{"items":{"$ref":"#/definitions/PlaybookResourceFilter"},"type":"array"},"parameters":{"items":{"$schema":"http://json-schema.org/draft-04/schema#","$ref":"#/definitions/PlaybookParameter"},"type":"array"},"actions":{"items":{"$schema":"http://json-schema.org/draft-04/schema#","$ref":"#/definitions/PlaybookAction"},"type":"array"},"approval":{"$schema":"http://json-schema.org/draft-04/schema#","$ref":"#/definitions/PlaybookApproval"}},"additionalProperties":false,"type":"object"},"PlaybookStatus":{"properties":{},"additionalProperties":false,"type":"object"},"PodAction":{"required":["name","spec"],"properties":{"name":{"type":"string"},"maxLength":{"type":"integer"},"timeout":{"type":"integer"},"spec":{"additionalProperties":true}},"additionalProperties":false,"type":"object"},"SQLAction":{"required":["query","driver"],"properties":{"connection":{"type":"string"},"url":{"type":"string"},"query":{"type":"string"},"driver":{"type":"string"}},"additionalProperties":false,"type":"object"},"SecretKeySelector":{"required":["key"],"properties":{"name":{"type":"string"},"key":{"type":"string"}},"additionalProperties":false,"type":"object"},"Time":{"properties":{},"additionalProperties":false,"type":"object"}}} \ No newline at end of file diff --git a/k8s/kommons.go b/k8s/kommons.go index 4c620c046..2d64f4233 100644 --- a/k8s/kommons.go +++ b/k8s/kommons.go @@ -1,67 +1,65 @@ package k8s import ( - "context" - "os" - "path/filepath" + "bytes" + "fmt" + "io" + "time" - "github.com/flanksource/commons/files" - "github.com/flanksource/commons/logger" - "github.com/flanksource/kommons" - "github.com/pkg/errors" - "gopkg.in/yaml.v2" + "github.com/flanksource/incident-commander/api" + v1 "k8s.io/api/core/v1" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" - "k8s.io/client-go/kubernetes" - "k8s.io/client-go/kubernetes/fake" - "k8s.io/client-go/rest" - "k8s.io/client-go/tools/clientcmd" - "k8s.io/client-go/util/homedir" ) -func NewKommonsClient() (*kommons.Client, kubernetes.Interface, error) { - kubeConfig := GetKubeconfig() - config, err := clientcmd.BuildConfigFromFlags("", kubeConfig) - if err != nil { - return nil, fake.NewSimpleClientset(), errors.Wrap(err, "Failed to generate rest config") - } - Client := kommons.NewClient(config, logger.StandardLogger()) - if Client == nil { - return nil, fake.NewSimpleClientset(), errors.New("could not create kommons client") - } +// WaitForPod waits for a pod to be in the specified phase, or returns an +// error if the timeout is exceeded +func WaitForPod(ctx api.Context, name string, timeout time.Duration, phases ...v1.PodPhase) error { + pods := ctx.Kubernetes().CoreV1().Pods(ctx.Namespace()) + start := time.Now() + for { + pod, err := pods.Get(ctx, name, metav1.GetOptions{}) + if start.Add(timeout).Before(time.Now()) { + return fmt.Errorf("timeout exceeded waiting for %s is %s, error: %v", name, pod.Status.Phase, err) + } + + if pod == nil || pod.Status.Phase == v1.PodPending { + time.Sleep(5 * time.Second) + continue + } + if pod.Status.Phase == v1.PodFailed { + return nil + } - k8s, err := Client.GetClientset() - if err == nil { - return Client, k8s, nil + for _, phase := range phases { + if pod.Status.Phase == phase { + return nil + } + } } - return nil, fake.NewSimpleClientset(), errors.Wrap(err, "failed to create k8s client") } -func GetClusterName(config *rest.Config) string { - clientset, err := kubernetes.NewForConfig(config) - if err != nil { - return "" +func GetPodLogs(ctx api.Context, podName, container string) (string, error) { + podLogOptions := v1.PodLogOptions{} + if container != "" { + podLogOptions.Container = container } - kubeadmConfig, err := clientset.CoreV1().ConfigMaps("kube-system").Get(context.TODO(), "kubeadm-config", metav1.GetOptions{}) + + req := ctx.Kubernetes().CoreV1().Pods(ctx.Namespace()).GetLogs(podName, &podLogOptions) + podLogs, err := req.Stream(ctx) if err != nil { - return "" + return "", err } - clusterConfiguration := make(map[string]interface{}) + defer podLogs.Close() - if err := yaml.Unmarshal([]byte(kubeadmConfig.Data["ClusterConfiguration"]), &clusterConfiguration); err != nil { - return "" + buf := new(bytes.Buffer) + _, err = io.Copy(buf, podLogs) + if err != nil { + return "", err } - return clusterConfiguration["clusterName"].(string) + + return buf.String(), nil } -func GetKubeconfig() string { - var kubeConfig string - if os.Getenv("KUBECONFIG") != "" { - kubeConfig = os.Getenv("KUBECONFIG") - } else if home := homedir.HomeDir(); home != "" { - kubeConfig = filepath.Join(home, ".kube", "config") - if !files.Exists(kubeConfig) { - kubeConfig = "" - } - } - return kubeConfig +func DeletePod(ctx api.Context, name string) error { + return ctx.Kubernetes().CoreV1().Pods(ctx.Namespace()).Delete(ctx, name, metav1.DeleteOptions{}) } diff --git a/playbook/actions/http.go b/playbook/actions/http.go index 354863b97..6b05c1eed 100644 --- a/playbook/actions/http.go +++ b/playbook/actions/http.go @@ -4,7 +4,6 @@ import ( "fmt" netHTTP "net/http" "net/url" - "time" "github.com/flanksource/commons/http" "github.com/flanksource/duty/models" @@ -17,25 +16,16 @@ type HTTPResult struct { Code string Headers netHTTP.Header Body string - SslAge *time.Duration } type HTTP struct { } func (c *HTTP) Run(ctx api.Context, action v1.HTTPAction, env TemplateEnv) (*HTTPResult, error) { - if action.URL == "" { - return nil, fmt.Errorf("must specify URL") - } - connection, err := ctx.HydrateConnection(action.HTTPConnection.Connection) if err != nil { - return nil, fmt.Errorf("must specify URL") + return nil, fmt.Errorf("failed to hydrate connection: %w", err) } else if connection != nil { - if connection.URL == "" { - return nil, fmt.Errorf("no url or connection specified") - } - if ntlm, ok := connection.Properties["ntlm"]; ok { action.NTLM = ntlm == "true" } else if ntlm, ok := connection.Properties["ntlmv2"]; ok { @@ -43,7 +33,7 @@ func (c *HTTP) Run(ctx api.Context, action v1.HTTPAction, env TemplateEnv) (*HTT } if _, err := url.Parse(connection.URL); err != nil { - return nil, fmt.Errorf("failed to parse url: %w", err) + return nil, fmt.Errorf("failed to parse url(%q): %w", connection.URL, err) } } else if connection == nil { connection = &models.Connection{ @@ -51,6 +41,10 @@ func (c *HTTP) Run(ctx api.Context, action v1.HTTPAction, env TemplateEnv) (*HTT } } + if connection.URL == "" { + return nil, fmt.Errorf("must specify a URL") + } + if action.TemplateBody { templated, err := gomplate.RunTemplate(env.AsMap(), gomplate.Template{Template: action.Body}) if err != nil { @@ -62,15 +56,18 @@ func (c *HTTP) Run(ctx api.Context, action v1.HTTPAction, env TemplateEnv) (*HTT resp, err := c.makeRequest(ctx, action, connection) if err != nil { - return nil, fmt.Errorf("failed to parse url: %w", err) + return nil, fmt.Errorf("failed to make HTTP request: %w", err) + } + + body, err := resp.AsString() + if err != nil { + return nil, fmt.Errorf("failed to get response body: %w", err) } - body, _ := resp.AsString() result := &HTTPResult{ Code: resp.Status, Headers: resp.Header, Body: body, - SslAge: resp.GetSSLAge(), } return result, nil @@ -110,7 +107,7 @@ func (c *HTTP) makeRequest(ctx api.Context, action v1.HTTPAction, connection *mo response, err := req.Do(action.Method, connection.URL) if err != nil { - return nil, err + return nil, fmt.Errorf("failed to make HTTP request: %w", err) } return response, nil diff --git a/playbook/actions/pod.go b/playbook/actions/pod.go index d61616a48..697acde42 100644 --- a/playbook/actions/pod.go +++ b/playbook/actions/pod.go @@ -3,26 +3,22 @@ package actions import ( "encoding/json" "fmt" - "strings" "time" "github.com/flanksource/commons/logger" + "github.com/flanksource/duty/models" corev1 "k8s.io/api/core/v1" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" - "k8s.io/apimachinery/pkg/util/rand" "github.com/flanksource/incident-commander/api" v1 "github.com/flanksource/incident-commander/api/v1" + "github.com/flanksource/incident-commander/k8s" ) const ( - defaultMaxLength = 3000 - defaultContainerTimeout = 5 * time.Minute + defaultContainerTimeout = 30 * time.Minute - sidecarContainerName = "container-waiter" - containerImage = "ubuntu:jammy" - playbookActionSelector = "playbooks.mission-control.flanksource.com/actions" - playbookActionLabelValue = "playbooks-pod-action" + playbookPodActionLabelPrefix = "playbooks.mission-control.flanksource.com" ) type PodResult struct { @@ -30,6 +26,7 @@ type PodResult struct { } type Pod struct { + PlaybookRun models.PlaybookRun } func (c *Pod) Run(ctx api.Context, action v1.PodAction, env TemplateEnv) (*PodResult, error) { @@ -38,26 +35,18 @@ func (c *Pod) Run(ctx api.Context, action v1.PodAction, env TemplateEnv) (*PodRe timeout = defaultContainerTimeout } - if action.MaxLength <= 0 { - action.MaxLength = defaultMaxLength - } - - pod, err := newPod(ctx, action) + pod, err := newPod(ctx, action, c.PlaybookRun) if err != nil { - return nil, err - } - - if err := cleanupExistingPods(ctx, fmt.Sprintf("%s=%s", playbookActionLabelValue, pod.Labels[playbookActionLabelValue])); err != nil { - return nil, err + return nil, fmt.Errorf("error creating pod struct: %w", err) } if _, err := ctx.Kubernetes().CoreV1().Pods(ctx.Namespace()).Create(ctx, pod, metav1.CreateOptions{}); err != nil { - return nil, err + return nil, fmt.Errorf("error creating pod: %w", err) } defer deletePod(ctx, pod) - if err := ctx.Kommons().WaitForPod(ctx.Namespace(), pod.Name, timeout, corev1.PodRunning, corev1.PodSucceeded, corev1.PodFailed); err != nil { - return nil, err + if err := k8s.WaitForPod(ctx, pod.Name, timeout, corev1.PodRunning, corev1.PodSucceeded, corev1.PodFailed); err != nil { + return nil, fmt.Errorf("error waiting for pod to complete: %w", err) } return &PodResult{ @@ -65,52 +54,39 @@ func (c *Pod) Run(ctx api.Context, action v1.PodAction, env TemplateEnv) (*PodRe }, nil } -func newPod(ctx api.Context, action v1.PodAction) (*corev1.Pod, error) { +func newPod(ctx api.Context, action v1.PodAction, playbookRun models.PlaybookRun) (*corev1.Pod, error) { pod := &corev1.Pod{} + pod.Name = fmt.Sprintf("%s-%s", action.Name, playbookRun.ID.String()) + pod.Namespace = ctx.Namespace() pod.APIVersion = corev1.SchemeGroupVersion.Version pod.Labels = map[string]string{ - playbookActionSelector: getPlaybookActionLabel(playbookActionLabelValue, action.Name, ctx.Namespace()), + newPodLabel("pod-action"): "true", + newPodLabel("action"): fmt.Sprintf("pod-action-%s-%s", action.Name, ctx.Namespace()), + newPodLabel("playbookRunID"): playbookRun.ID.String(), + newPodLabel("playbookID"): playbookRun.PlaybookID.String(), } - pod.Namespace = ctx.Namespace() - pod.Name = action.Name + "-" + strings.ToLower(rand.String(5)) if err := json.Unmarshal(action.Spec, &pod.Spec); err != nil { - return nil, err + return nil, fmt.Errorf("error unmarshalling pod spec: %w", err) } pod.Spec.RestartPolicy = corev1.RestartPolicyNever return pod, nil } func deletePod(ctx api.Context, pod *corev1.Pod) { - if err := ctx.Kommons().DeleteByKind("Pod", pod.Namespace, pod.Name); err != nil { + if err := k8s.DeletePod(ctx, pod.Name); err != nil { logger.Warnf("failed to delete pod %s/%s: %v", pod.Namespace, pod.Name, err) } } func getLogs(ctx api.Context, pod *corev1.Pod, maxLength int) string { - message, _ := ctx.Kommons().GetPodLogs(pod.Namespace, pod.Name, pod.Spec.Containers[0].Name) - if len(message) > maxLength { + message, _ := k8s.GetPodLogs(ctx, pod.Name, pod.Spec.Containers[0].Name) + if maxLength > 0 { message = message[len(message)-maxLength:] } return message } -func getPlaybookActionLabel(label, name, namespace string) string { - return fmt.Sprintf("%v-%v-%v", label, name, namespace) -} - -func cleanupExistingPods(ctx api.Context, selector string) error { - pods := ctx.Kubernetes().CoreV1().Pods(ctx.Namespace()) - existingPods, err := pods.List(ctx, metav1.ListOptions{ - LabelSelector: selector, - }) - if err != nil { - return err - } - - for _, junitPod := range existingPods.Items { - deletePod(ctx, &junitPod) - } - - return nil +func newPodLabel(key string) string { + return fmt.Sprintf("%s/%s", playbookPodActionLabelPrefix, key) } diff --git a/playbook/runner.go b/playbook/runner.go index 2629ef2f2..c95436217 100644 --- a/playbook/runner.go +++ b/playbook/runner.go @@ -140,7 +140,9 @@ func executeAction(ctx api.Context, run models.PlaybookRun, action v1.PlaybookAc } if action.Pod != nil { - var e actions.Pod + e := actions.Pod{ + PlaybookRun: run, + } res, err := e.Run(ctx, *action.Pod, env) if err != nil { return nil, err