[
  {
    "path": ".gitignore",
    "content": "# untrak binary\nuntrak\n\n# Binaries for programs and plugins\n*.exe\n*.exe~\n*.dll\n*.so\n*.dylib\n\n# Test binary, build with `go test -c`\n*.test\n\n# Output of the go coverage tool, specifically when used with LiteIDE\n*.out\n"
  },
  {
    "path": ".travis.yml",
    "content": "language: go\ngo:\n- 1.14.x\nscript:\n  - go get -v -d ./...\n  - GOOS=linux GOARCH=amd64 go build -o untrak-linux\n  - GOOS=darwin GOARCH=amd64 go build -o untrak-darwin\ndeploy:\n  provider: releases\n  api_key:\n    secure: u76RGeXFqXOBTOiEFChuXS7C1aaLl7vX8J39hOyla22LAyIeSOmOK6atwMfz6zq3cui9wo/bQ6Mke3p5Fzm1bhvA1srLo+Ma59JYEzsMvyDE0A7OZG7W8S+iJYEn/rlLGBzjAozg0TlmOMh8s/H05TyUEVOOqFwlEyb/O4H4Zg/ooJdNv0A+MLYGqgCiyyDv2tuMzY6GNeiBLrR69iAJfEjkrOfzC+dnVKEHi9K6OV1UEWX/63VHd2SfvJmG9vOL++3QH05ToMP0Geuek0fvQ0c5EYjAZYW4d4zw3dxXsHLwN7GRjQuos0uGXGqeHaSkYfSbtDQhLNV3FRjXlp8r4fhjhHcWQm4GO5tZItMMWVFwA8S1v+aiZ4B2NwrewwOxRzyBqtYGV16yklkHngOwsbU3596TKQeWv9v7sCw7f1N+uoAYmbyf7Si95WZnvkdIW1DvADg/32+dOcsLOKWFo1wlkoA44O8qOUetfHFd9DDApvd1n6XfvsWkejOdk8X02R/1XB3fCFRxONegIsVXjhlodBS0AzoF398goDfiDtj9TCdOwHUMIGFGZ/SARJRFwkCNkzY46B1/P4yH/vzrxIsALAuPDGUjqfrERtpl289M/0SNz4j7hhDgVYq+BLzipErDMmT+3l+RRTR9rKcHWN2NNm2zkDNoNRVvyiZtpp0=\n  file:\n    - untrak-linux\n    - untrak-darwin\n  skip_cleanup: true\n  on:\n    tags: true\n    all_branches: true\n"
  },
  {
    "path": "CHANGELOG.md",
    "content": "# Untrak Changelog\n\n## v0.2.0 - 2020-10-22\nThanks to @almariah !\n\n* Support building osx binaries on tags (#4)\n* Compare only name and kind if the resourceIn is not namespaced (#4)\n* Support flag to fail on untracked resources (#4)\n* Add support for env variables to be substitute in args (#4)\n\n## v0.1.1 - 2020-05-27\n* Fix yaml parsing bug\n\n## v0.1.0 - 2019-03-26\n* Initial Release"
  },
  {
    "path": "LICENSE",
    "content": "MIT License\n\nCopyright (c) 2019 Yann Coleu\n\nPermission is hereby granted, free of charge, to any person obtaining a copy\nof this software and associated documentation files (the \"Software\"), to deal\nin the Software without restriction, including without limitation the rights\nto use, copy, modify, merge, publish, distribute, sublicense, and/or sell\ncopies of the Software, and to permit persons to whom the Software is\nfurnished to do so, subject to the following conditions:\n\nThe above copyright notice and this permission notice shall be included in all\ncopies or substantial portions of the Software.\n\nTHE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR\nIMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,\nFITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE\nAUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER\nLIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,\nOUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE\nSOFTWARE.\n"
  },
  {
    "path": "README.md",
    "content": "# Untrak\nFind untracked resources in Kubernetes cluster, garbage collect them.\n\n[![Build Status](https://travis-ci.org/yanc0/untrak.svg?branch=master)](https://travis-ci.org/yanc0/untrak)\n\n## Why?\n\nWhen you use `kubectl apply`, `kustomize build` or `helm template` for injecting manifests through you CI/CD pipeline, kubernetes doesn't know when an object has been deleted from your repo. Your resources are now **untracked** from your delivery process and they are still managed by your clusters.\n\nUntrak is a tool made for finding and deleting these untracked files on your cluster.\n\n\n## How it works?\n\n![untrak-schema.png](docs/untrak-schema.png)\n\nVia a simple config file (`untrak.yaml`), this tool will internally execute commands that output YAMLs and find resources in your clusters that are not in your SCM anymore.\n\nIn a GitOps context, this is the tool you always dreamed of.\n\n## Installation\n\nDownload latest version on [releases page](https://github.com/yanc0/untrak/releases)\n\n- `chmod +x untrak`\n- `sudo mv untrak /usr/local/bin`\n- `untrak --help`\n\n## Example\n\nPut a `untrak.yaml` file in you SCM.\n\n> Note: if you have multiple environments, you would need multiple untrak config files.\n```yaml\n# untrak.yaml\n## git sources\nin:\n- cmd: \"cat\"\n  args: [\"example/manifests/resources.yaml\"]\n\n## cluster manifests\nout:\n- cmd: \"kubectl\"\n  args: [\"get\", \"cm,deploy,svc,ing\", \"-o\", \"yaml\", \"-n\", \"api\"]\n\nexclude:\n- namespace\n```\nTo show untracked resources in your cluster (out) simply launch `untrak` like so:\n\n```\n$ untrak -c untrak.yaml -o text\n- api/ConfigMap/django-config-b4k42gm792\n- api/ConfigMap/django-config-g55mctg456\n- api/Ingress/my-ingress\n```\n\nIf your manifests have the namespace set to non-namespaced resource, untrak will skip the namespace. A list of supported non-namespaced resource types that will be skipped are defined by default. If you have installed more non-namespaced resource types (eg., `CRDs`), you could add extra resource types to skip namespace in comparison:\n```yaml\n# untrak.yaml\n## git sources\nin:\n...\n\n## cluster manifests\nout:\n...\n\nnonNamespaced:\n- some_crd_type\n```\n\nYou can use environment variables on command arguments (in/out):\n```yaml\nin:\n- cmd: \"cat\"\n  args: [\"example/$SOME_FILE_NAME\"]\n...\n```\n\nIf you need to garbage collect them, you can change the output format to yaml and pipe the result in kubectl:\n\n```\n$ untrak -c untrak.yaml -o yaml | kubectl delete -f -\nconfigmap \"django-config-b4k42gm792\" deleted\nconfigmap \"django-config-g55mctg456\" deleted\ningress.extensions \"my-ingress\" deleted\n```\n\nIf you want to fail on untracked resources (exit status 1), you can use `-fail`:\n\n```\n$ untrak -c untrak.yaml -fail\n```\n\n> **Caution**: please test this tool extensively before deleting resources. The software is provided \"as is\", without warranty of any kind.\n"
  },
  {
    "path": "config/loader.go",
    "content": "package config\n\nimport (\n\t\"io/ioutil\"\n\n\tyaml \"gopkg.in/yaml.v2\"\n)\n\n// Load untrak config from path\nfunc Load(path string) (*Config, error) {\n\tvar cfg Config\n\n\tcontent, err := ioutil.ReadFile(path)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\terr = yaml.Unmarshal(content, &cfg)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\treturn &cfg, nil\n}\n"
  },
  {
    "path": "config/structs.go",
    "content": "package config\n\ntype CommandConfig struct {\n\tCmd  string   `yaml:\"cmd\"`\n\tArgs []string `yaml:\"args\"`\n}\n\ntype Config struct {\n\tIn            []*CommandConfig `yaml:\"in\"`\n\tOut           []*CommandConfig `yaml:\"out\"`\n\tExclude       []string         `yaml:\"exclude\"`\n\tNonNamespaced []string         `yaml:\"nonNamespaced\"`\n}\n"
  },
  {
    "path": "example/manifests/resources_in.yaml",
    "content": "---\napiVersion: v1\nkind: Namespace\nmetadata:\n  name: app\n  namespace: app\n---\napiVersion: v1\nkind: Service\nmetadata:\n  labels:\n    app: django\n    env: dev\n  name: django\n  namespace: api\nspec:\n  ports:\n  - name: http\n    port: 80\n    targetPort: 8000\n  selector:\n    app: django\n    env: dev\n  type: ClusterIP\n---\napiVersion: apps/v1\nkind: Deployment\nmetadata:\n  labels:\n    app: django\n    env: dev\n  name: django\n  namespace: api\nspec:\n  replicas: 1\n  selector:\n    matchLabels:\n      app: django\n      env: dev\n  template:\n    metadata:\n      labels:\n        app: django\n        env: dev\n    spec:\n      containers:\n      - image: eu.gcr.io/example/django\n        imagePullPolicy: Always\n        livenessProbe:\n          failureThreshold: 20\n          httpGet:\n            path: /liveliness\n            port: 8000\n          initialDelaySeconds: 10\n          periodSeconds: 3\n          timeoutSeconds: 5\n        name: django\n        readinessProbe:\n          failureThreshold: 1\n          httpGet:\n            path: /readiness\n            port: 8000\n          initialDelaySeconds: 10\n          periodSeconds: 5\n          timeoutSeconds: 5\n---"
  },
  {
    "path": "example/manifests/resources_out.yaml",
    "content": "---\napiVersion: v1\nkind: Service\nmetadata:\n  labels:\n    app: django\n    env: dev\n  name: django\n  namespace: api\nspec:\n  ports:\n  - name: http\n    port: 80\n    targetPort: 8000\n  selector:\n    app: django\n    env: dev\n  type: ClusterIP\n---\n---\n---\napiVersion: apps/v1\nkind: Deployment\nmetadata:\n  labels:\n    app: django\n    env: dev\n  name: django\n  namespace: api\nspec:\n  replicas: 1\n  selector:\n    matchLabels:\n      app: django\n      env: dev\n  template:\n    metadata:\n      labels:\n        app: django\n        env: dev\n    spec:\n      containers:\n      - image: eu.gcr.io/example/django\n        imagePullPolicy: Always\n        livenessProbe:\n          failureThreshold: 20\n          httpGet:\n            path: /liveliness\n            port: 8000\n          initialDelaySeconds: 10\n          periodSeconds: 3\n          timeoutSeconds: 5\n        name: django\n        readinessProbe:\n          failureThreshold: 1\n          httpGet:\n            path: /readiness\n            port: 8000\n          initialDelaySeconds: 10\n          periodSeconds: 5\n          timeoutSeconds: 5\n---\napiVersion: extensions/v1beta1\nkind: Ingress\nmetadata:\n  annotations:\n    kubernetes.io/ingress.class: nginx\n  labels:\n    app: django\n    env: dev\n  name: django\n  namespace: api\nspec:\n  rules:\n  - host: django.example.com\n    http:\n      paths:\n      - backend:\n          serviceName: django\n          servicePort: 80\n        path: /\n---\napiVersion: v1\nkind: Namespace\nmetadata:\n  name: app"
  },
  {
    "path": "go.mod",
    "content": "module github.com/yanc0/untrak\n\ngo 1.12\n\nrequire gopkg.in/yaml.v2 v2.3.0\n"
  },
  {
    "path": "go.sum",
    "content": "gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM=\ngopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=\ngopkg.in/yaml.v2 v2.3.0 h1:clyUAQHOM3G0M3f5vQj7LuJrETvjVot3Z5el9nffUtU=\ngopkg.in/yaml.v2 v2.3.0/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=\n"
  },
  {
    "path": "kubernetes/non_namespaced.go",
    "content": "package kubernetes\n\nvar DefaultNonNamespacedResources = []string{\n\t\"componentstatuse\",\n\t\"namespace\",\n\t\"node\",\n\t\"persistentvolume\",\n\t\"mutatingwebhookconfiguration\",\n\t\"validatingwebhookconfiguration\",\n\t\"customresourcedefinition\",\n\t\"apiservice\",\n\t\"certificatesigningrequest\",\n\t\"runtimeclass\",\n\t\"podsecuritypolicy\",\n\t\"clusterrolebinding\",\n\t\"clusterrole\",\n\t\"priorityclass\",\n\t\"csidriver\",\n\t\"csinode\",\n\t\"storageclass\",\n\t\"volumeattachment\",\n}"
  },
  {
    "path": "kubernetes/structs.go",
    "content": "package kubernetes\n\nimport (\n\t\"fmt\"\n)\n\n// Metadata of kubernetes resource\ntype Metadata struct {\n\tName      string `yaml:\"name\"`\n\tNamespace string `yaml:\"namespace,omitempty\"`\n}\n\n// Resource is a minimal description of a kubernetes object\ntype Resource struct {\n\tAPIVersion string      `yaml:\"apiVersion\"`\n\tKind       string      `yaml:\"kind\"`\n\tMetadata   *Metadata   `yaml:\"metadata\"`\n\tItems      []*Resource `yaml:\"items,omitempty\"`\n}\n\n// ID of the resource\nfunc (r *Resource) ID() string {\n\treturn fmt.Sprintf(\"%s/%s/%s\",\n\t\tr.Metadata.Namespace,\n\t\tr.Kind,\n\t\tr.Metadata.Name,\n\t)\n}\n\n//Empty return true if resource was not correctly loaded\nfunc (r *Resource) Empty() bool {\n\treturn r.APIVersion == \"\" ||\n\t\tr.Kind == \"\" ||\n\t\tr.Metadata == nil\n}"
  },
  {
    "path": "main.go",
    "content": "package main\n\nimport (\n\t\"bytes\"\n\t\"flag\"\n\t\"io\"\n\t\"log\"\n\t\"os\"\n\t\"os/exec\"\n\t\"sync\"\n\n\t\"github.com/yanc0/untrak/outputs\"\n\t\"github.com/yanc0/untrak/utils\"\n\tyaml \"gopkg.in/yaml.v2\"\n\n\t\"github.com/yanc0/untrak/kubernetes\"\n\n\t\"github.com/yanc0/untrak/config\"\n)\n\nfunc main() {\n\t// Flags, command line parameters\n\tvar cfgPathOpt = flag.String(\"config\", \"./untrak.yaml\", \"untrak Config Path\")\n\tvar outputOpt = flag.String(\"o\", \"text\", \"Output format\")\n\tvar failOpt = flag.Bool(\"fail\", false, \"Fail on untracked resources\")\n\tflag.Parse()\n\n\tvar wg sync.WaitGroup\n\tvar resourcesIn []*kubernetes.Resource\n\tvar resourcesOut []*kubernetes.Resource\n\n\t// Config Load\n\tcfg, err := config.Load(*cfgPathOpt)\n\tif err != nil {\n\t\tlog.Printf(\"[ERR] Cannot load %s file: %v\\n\", *cfgPathOpt, err)\n\t\tos.Exit(1)\n\t}\n\n\tcfg.NonNamespaced = append(cfg.NonNamespaced, kubernetes.DefaultNonNamespacedResources...)\n\n\twg.Add(1)\n\tgo func() {\n\t\tdefer wg.Done()\n\t\tresourcesIn, err = getKubernetesResources(cfg.In)\n\t\tif err != nil {\n\t\t\tlog.Printf(\"[ERR] Failed to get Kubernetes resources (in): %v\\n\", err)\n\t\t\tos.Exit(1)\n\t\t}\n\t}()\n\n\twg.Add(1)\n\tgo func() {\n\t\tdefer wg.Done()\n\t\tresourcesOut, err = getKubernetesResources(cfg.Out)\n\t\tif err != nil {\n\t\t\tlog.Printf(\"[ERR] Failed to get Kubernetes resources (out): %v\\n\", err)\n\t\t\tos.Exit(1)\n\t\t}\n\t}()\n\n\twg.Wait()\n\n\tuntrackedResources := listUntrackedResources(resourcesIn, resourcesOut, cfg.Exclude, cfg.NonNamespaced)\n\tswitch {\n\tcase *outputOpt == \"text\":\n\t\toutputs.Text(untrackedResources)\n\tcase *outputOpt == \"yaml\":\n\t\toutputs.YAML(untrackedResources)\n\tdefault:\n\t\toutputs.Text(untrackedResources)\n\t}\n\n\tif len(untrackedResources) > 0 && *failOpt {\n\t\tos.Exit(1)\n\t}\n}\n\nfunc getKubernetesResources(cfgs []*config.CommandConfig) ([]*kubernetes.Resource, error) {\n\tconst yamlSeparator = \"---\\n\"\n\tvar resources []*kubernetes.Resource\n\n\tvar wg sync.WaitGroup\n\tvar mutex = &sync.Mutex{}\n\n\tfor _, cfg := range cfgs {\n\t\twg.Add(1)\n\t\tgo func(cmd string, args ...string) {\n\t\t\tdefer wg.Done()\n\n\t\t\t// substitute env variables if any has been set\n\t\t\tfor i, _ := range args {\n\t\t\t\targs[i] = os.ExpandEnv(args[i])\n\t\t\t}\n\n\t\t\tc := exec.Command(cmd, args...)\n\t\t\tvar outb, errb bytes.Buffer\n\t\t\tc.Stdout = &outb\n\t\t\tc.Stderr = &errb\n\t\t\terr := c.Run()\n\t\t\tif err != nil {\n\t\t\t\tlog.Fatal(err, errb.String())\n\t\t\t}\n\t\t\tstdoutDec := yaml.NewDecoder(&outb)\n\t\t\tfor {\n\t\t\t\ttempResource := &kubernetes.Resource{}\n\t\t\t\terr := stdoutDec.Decode(tempResource)\n\t\t\t\tif err != nil && err != io.EOF {\n\t\t\t\t\tlog.Printf(\"[ERR] Failed to decode yaml stream: %s\\n\", err.Error())\n\t\t\t\t\tos.Exit(1)\n\t\t\t\t}\n\t\t\t\tif err == io.EOF {\n\t\t\t\t\tbreak\n\t\t\t\t}\n\t\t\t\tif tempResource.Kind == \"List\" {\n\t\t\t\t\tmutex.Lock()\n\t\t\t\t\tresources = append(resources, tempResource.Items...)\n\t\t\t\t\tmutex.Unlock()\n\t\t\t\t\tcontinue\n\t\t\t\t}\n\t\t\t\t// Resource can be empty if yaml file has return lines, separators or comments\n\t\t\t\t// for example:\n\t\t\t\t// # empty resource\n\t\t\t\t// ---\n\t\t\t\t// ---\n\t\t\t\t// YAML decoder consider these lines valid but resource will be uninitialized\n\t\t\t\tif !tempResource.Empty() {\n\t\t\t\t\tmutex.Lock()\n\t\t\t\t\tresources = append(resources, tempResource)\n\t\t\t\t\tmutex.Unlock()\n\t\t\t\t}\n\t\t\t}\n\t\t}(cfg.Cmd, cfg.Args...)\n\t}\n\twg.Wait()\n\treturn resources, nil\n}\n\nfunc listUntrackedResources(in []*kubernetes.Resource, out []*kubernetes.Resource, kindExclude []string, nonNamespaced []string) []*kubernetes.Resource {\n\tvar untrackedResources []*kubernetes.Resource\n\tfor _, resourceOut := range out {\n\t\t// Resource is in the exlude list, skip it\n\t\tif utils.StringInListCaseInsensitive(kindExclude, resourceOut.Kind) {\n\t\t\tcontinue\n\t\t}\n\t\tfound := false\n\t\tfor _, resourceIn := range in {\n\n\t\t\t// If input resource is not namespaced, compare only kind and Name\n\t\t\tif utils.StringInListCaseInsensitive(nonNamespaced, resourceIn.Kind) {\n\t\t\t\tif resourceOut.Kind == resourceIn.Kind && resourceOut.Metadata.Name == resourceIn.Metadata.Name {\n\t\t\t\t\tfound = true\n\t\t\t\t\tbreak\n\t\t\t\t}\n\t\t\t}\n\n\t\t\t// If resource has been found in both IN an OUT, there is nothing to do\n\t\t\tif resourceOut.ID() == resourceIn.ID() {\n\t\t\t\tfound = true\n\t\t\t\tbreak\n\t\t\t}\n\t\t}\n\t\t// If resource OUT is not found in IN, it is untracked\n\t\tif !found {\n\t\t\tuntrackedResources = append(untrackedResources, resourceOut)\n\t\t}\n\t}\n\n\treturn untrackedResources\n}\n"
  },
  {
    "path": "outputs/text.go",
    "content": "package outputs\n\nimport (\n\t\"fmt\"\n\n\t\"github.com/yanc0/untrak/kubernetes\"\n)\n\n// Text output resources as text\nfunc Text(resources []*kubernetes.Resource) {\n\tvar output string\n\tfor _, r := range resources {\n\t\tout := r.ID()\n\t\toutput += fmt.Sprintf(\"- %s\\n\", out)\n\t}\n\tfmt.Println(output)\n}\n"
  },
  {
    "path": "outputs/yaml.go",
    "content": "package outputs\n\nimport (\n\t\"fmt\"\n\n\t\"github.com/yanc0/untrak/kubernetes\"\n\tyaml \"gopkg.in/yaml.v2\"\n)\n\n// YAML outputs resources as YAML\nfunc YAML(resources []*kubernetes.Resource) {\n\tvar output string\n\tfor _, r := range resources {\n\t\tout, err := yaml.Marshal(r)\n\t\tif err != nil {\n\t\t\tpanic(err)\n\t\t}\n\t\toutput += fmt.Sprintf(\"---\\n%s\\n\", string(out))\n\t}\n\tfmt.Printf(\"%s\", output)\n}\n"
  },
  {
    "path": "untrak.yaml",
    "content": "---\n# Untrak configuration\n# All commands must produce YAML output on stdout either as:\n#  - Kind: List - List Kubernetes resource type(from kubectl get stdout)\n#  - Concatenated YAML with \"---\" separator\n###\n\n# Kubernetes resources from your versionned controlled configuration\nin:\n- cmd: \"cat\"\n  args: [\"example/manifests/resources_in.yaml\"]\nout:\n- cmd: \"cat\"\n  args: [\"example/manifests/resources_out.yaml\"]\n\n# Kubernetes resources on your cluster\n# check only configmaps, deployments, services and ingresses in api namespace\n# out:\n# - cmd: \"kubectl\"\n#   args: [\"get\", \"cm,deploy,svc,ing\", \"-o\", \"yaml\", \"-n\", \"api\"]\n\n# You can use environment variables on command args\n# out:\n# - cmd: \"cat\"\n#   args: [\"example/$SOME_FILE_NAME\"]\n\n# You can exclude some resource type from the comparison\nexclude:\n- namespace\n- secret\n- configmap\n\n# Declare non-namespaced resource types to be considered in resource comparison.\n# There are some defined resource types by default by default like namespace,\n# node, clusterrole, etc.\n# nonNamespaced:\n# - some_crd_type\n"
  },
  {
    "path": "utils/commands.go",
    "content": "package utils\n\nimport (\n\t\"bytes\"\n\t\"os/exec\"\n)\n\n// Exec exec the command + args and returns stdout and stderr\nfunc Exec(cmd string, args ...string) ([]byte, []byte, error) {\n\tc := exec.Command(cmd, args...)\n\n\tstdout := &bytes.Buffer{}\n\tstderr := &bytes.Buffer{}\n\tc.Stdout = stdout\n\tc.Stderr = stderr\n\n\terr := c.Run()\n\tif err != nil {\n\t\treturn stdout.Bytes(), stderr.Bytes(), err\n\t}\n\treturn stdout.Bytes(), stderr.Bytes(), nil\n}\n"
  },
  {
    "path": "utils/strings.go",
    "content": "package utils\n\nimport (\n\t\"strings\"\n)\n\n// StringInListCaseInsensitive return true if str is in the list (case insensitive)\nfunc StringInListCaseInsensitive(list []string, str string) bool {\n\tfor _, s := range list {\n\t\tif strings.ToLower(s) == strings.ToLower(str) {\n\t\t\treturn true\n\t\t}\n\t}\n\treturn false\n}\n"
  }
]