Merge branch 'logging' into activity-logs

This commit is contained in:
Vladyslav Matsiiako
2022-12-17 20:22:28 -05:00
34 changed files with 952 additions and 30 deletions

View File

@ -0,0 +1,38 @@
name: Release Docker image for K8 operator
on: [workflow_dispatch]
jobs:
release:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v2
- name: 🔧 Set up QEMU
uses: docker/setup-qemu-action@v1
- name: 🔧 Set up Docker Buildx
uses: docker/setup-buildx-action@v1
- name: 🐋 Login to Docker Hub
uses: docker/login-action@v1
with:
username: ${{ secrets.DOCKERHUB_USERNAME }}
password: ${{ secrets.DOCKERHUB_TOKEN }}
- name: Build and push
id: docker_build
uses: docker/build-push-action@v2
with:
context: k8-operator
push: true
platforms: linux/amd64,linux/arm64
tags: infisical/kubernetes-operator:latest
- uses: actions/setup-go@v2
- name: Upload CRD manifest
uses: svenstaro/upload-release-action@v2
with:
repo_token: ${{ secrets.GITHUB_TOKEN }}
file: dist/install-secrets-operator.yaml
tag: ${{ github.ref }}

View File

@ -13,6 +13,7 @@ import * as stripeController from './stripeController';
import * as userActionController from './userActionController';
import * as userController from './userController';
import * as workspaceController from './workspaceController';
import * as logController from './logController';
export {
authController,
@ -29,5 +30,6 @@ export {
stripeController,
userActionController,
userController,
workspaceController
workspaceController,
logController
};

View File

@ -0,0 +1,30 @@
import { Request, Response } from 'express';
import * as Sentry from '@sentry/node';
import {
Log
} from '../models';
export const getLogs = async (req: Request, res: Response) => {
// get logs
console.log('getLogs');
let logs;
try {
const { workspaceId } = req.params;
logs = await Log.find({
workspace: workspaceId
});
} catch (err) {
Sentry.setUser({ email: req.user.email });
Sentry.captureException(err);
return res.status(400).send({
message: 'Failed to get audit logs'
});
}
return res.status(200).send({
logs
});
}

View File

@ -38,7 +38,8 @@ import {
password as passwordRouter,
stripe as stripeRouter,
integration as integrationRouter,
integrationAuth as integrationAuthRouter
integrationAuth as integrationAuthRouter,
log as logRouter
} from './routes';
const connectWithRetry = () => {
@ -92,6 +93,7 @@ app.use('/api/v1/password', passwordRouter);
app.use('/api/v1/stripe', stripeRouter);
app.use('/api/v1/integration', integrationRouter);
app.use('/api/v1/integration-auth', integrationAuthRouter);
app.use('/api/v1/log', logRouter);
const server = http.createServer(app);

View File

@ -12,6 +12,7 @@ import Token, { IToken } from './token';
import User, { IUser } from './user';
import UserAction, { IUserAction } from './userAction';
import Workspace, { IWorkspace } from './workspace';
import Log, { ILog } from './log';
export {
BackupPrivateKey,
@ -41,5 +42,7 @@ export {
UserAction,
IUserAction,
Workspace,
IWorkspace
IWorkspace,
Log,
ILog
};

46
backend/src/models/log.ts Normal file
View File

@ -0,0 +1,46 @@
import { Schema, model, Types } from 'mongoose';
export interface ILog {
_id: Types.ObjectId;
user: Types.ObjectId;
workspace: Types.ObjectId;
event: string;
source: string;
ipAddress: string;
}
// TODO: need a way to store payload info for each
// log
// which secret is being ref etc.
const logSchema = new Schema<ILog>(
{
user: {
type: Schema.Types.ObjectId,
ref: 'User'
},
workspace: {
type: Schema.Types.ObjectId,
ref: 'Workspace'
},
event: {
type: String,
required: true
},
source: { // should this just be a payload attr?
type: String,
required: true
},
ipAddress: { // store in bytes?
type: String,
required: true
}
}, {
timestamps: true
}
);
const Log = model<ILog>('Log', logSchema);
export default Log;

View File

@ -14,6 +14,7 @@ import password from './password';
import stripe from './stripe';
import integration from './integration';
import integrationAuth from './integrationAuth';
import log from './log';
export {
signup,
@ -31,5 +32,6 @@ export {
password,
stripe,
integration,
integrationAuth
integrationAuth,
log
};

17
backend/src/routes/log.ts Normal file
View File

@ -0,0 +1,17 @@
import express from 'express';
const router = express.Router();
import {
requireAuth,
validateRequest
} from '../middleware';
import { logController } from '../controllers';
// TODO: workspaceId validation
router.get(
'/:workspaceId',
requireAuth,
validateRequest,
logController.getLogs
);
export default router;

View File

@ -0,0 +1,5 @@
---
title: "Circle CI"
---
Coming soon.

View File

@ -0,0 +1,5 @@
---
title: "Fly.io"
---
Coming soon.

View File

@ -0,0 +1,5 @@
---
title: "Render"
---
Coming soon.

View File

@ -13,12 +13,14 @@ Missing an integration? Throw in a [request](https://github.com/Infisical/infisi
| Kubernetes | Platform | Coming soon |
| [Heroku](/integrations/cloud/heroku) | Cloud | Available |
| [Vercel](/integrations/cloud/vercel) | Cloud | Coming soon |
| [Render](/integrations/cloud/render) | Cloud | Coming soon |
| [Fly.io](/integrations/cloud/flyio) | Cloud | Coming soon |
| AWS | Cloud | Coming soon |
| GCP | Cloud | Coming soon |
| Azure | Cloud | Coming soon |
| DigitalOcean | Cloud | Coming soon |
| GitLab | CI/CD | Coming soon |
| CircleCI | CI/CD | Coming soon |
| [CircleCI](/integrations/cicd/circleci) | CI/CD | Coming soon |
| TravisCI | CI/CD | Coming soon |
| GitHub Actions | CI/CD | Coming soon |
| Jenkins | CI/CD | Coming soon |

View File

@ -131,7 +131,15 @@
"group": "Cloud",
"pages": [
"integrations/cloud/heroku",
"integrations/cloud/vercel"
"integrations/cloud/vercel",
"integrations/cloud/render",
"integrations/cloud/flyio"
]
},
{
"group": "CI/CD",
"pages": [
"integrations/cicd/circleci"
]
},
{

View File

@ -14,3 +14,4 @@ helm install infisical-helm-charts/<name-of-helm-chart>
#### Available chart names
- infisical
- secrets-operator

View File

@ -0,0 +1,23 @@
# Patterns to ignore when building packages.
# This supports shell glob matching, relative path matching, and
# negation (prefixed with !). Only one pattern per line.
.DS_Store
# Common VCS dirs
.git/
.gitignore
.bzr/
.bzrignore
.hg/
.hgignore
.svn/
# Common backup files
*.swp
*.bak
*.tmp
*.orig
*~
# Various IDEs
.project
.idea/
*.tmproj
.vscode/

View File

@ -0,0 +1,21 @@
apiVersion: v2
name: secrets-operator
description: A Helm chart for Infisical secrets
# A chart can be either an 'application' or a 'library' chart.
#
# Application charts are a collection of templates that can be packaged into versioned archives
# to be deployed.
#
# Library charts provide useful utilities or functions for the chart developer. They're included as
# a dependency of application charts to inject those utilities and functions into the rendering
# pipeline. Library charts do not define any templates and therefore cannot be deployed.
type: application
# This is the chart version. This version number should be incremented each time you make changes
# to the chart and its templates, including the app version.
# Versions are expected to follow Semantic Versioning (https://semver.org/)
version: 0.1.0
# This is the version number of the application being deployed. This version number should be
# incremented each time you make changes to the application. Versions are not expected to
# follow Semantic Versioning. They should reflect the version the application is using.
# It is recommended to use it with quotes.
appVersion: "0.1.0"

View File

@ -0,0 +1,62 @@
{{/*
Expand the name of the chart.
*/}}
{{- define "secrets-operator.name" -}}
{{- default .Chart.Name .Values.nameOverride | trunc 63 | trimSuffix "-" }}
{{- end }}
{{/*
Create a default fully qualified app name.
We truncate at 63 chars because some Kubernetes name fields are limited to this (by the DNS naming spec).
If release name contains chart name it will be used as a full name.
*/}}
{{- define "secrets-operator.fullname" -}}
{{- if .Values.fullnameOverride }}
{{- .Values.fullnameOverride | trunc 63 | trimSuffix "-" }}
{{- else }}
{{- $name := default .Chart.Name .Values.nameOverride }}
{{- if contains $name .Release.Name }}
{{- .Release.Name | trunc 63 | trimSuffix "-" }}
{{- else }}
{{- printf "%s-%s" .Release.Name $name | trunc 63 | trimSuffix "-" }}
{{- end }}
{{- end }}
{{- end }}
{{/*
Create chart name and version as used by the chart label.
*/}}
{{- define "secrets-operator.chart" -}}
{{- printf "%s-%s" .Chart.Name .Chart.Version | replace "+" "_" | trunc 63 | trimSuffix "-" }}
{{- end }}
{{/*
Common labels
*/}}
{{- define "secrets-operator.labels" -}}
helm.sh/chart: {{ include "secrets-operator.chart" . }}
{{ include "secrets-operator.selectorLabels" . }}
{{- if .Chart.AppVersion }}
app.kubernetes.io/version: {{ .Chart.AppVersion | quote }}
{{- end }}
app.kubernetes.io/managed-by: {{ .Release.Service }}
{{- end }}
{{/*
Selector labels
*/}}
{{- define "secrets-operator.selectorLabels" -}}
app.kubernetes.io/name: {{ include "secrets-operator.name" . }}
app.kubernetes.io/instance: {{ .Release.Name }}
{{- end }}
{{/*
Create the name of the service account to use
*/}}
{{- define "secrets-operator.serviceAccountName" -}}
{{- if .Values.serviceAccount.create }}
{{- default (include "secrets-operator.fullname" .) .Values.serviceAccount.name }}
{{- else }}
{{- default "default" .Values.serviceAccount.name }}
{{- end }}
{{- end }}

View File

@ -0,0 +1,108 @@
apiVersion: v1
kind: ServiceAccount
metadata:
name: {{ include "secrets-operator.fullname" . }}-controller-manager
labels:
app.kubernetes.io/component: rbac
app.kubernetes.io/created-by: k8-operator
app.kubernetes.io/part-of: k8-operator
{{- include "secrets-operator.labels" . | nindent 4 }}
---
apiVersion: apps/v1
kind: Deployment
metadata:
name: {{ include "secrets-operator.fullname" . }}-controller-manager
labels:
app.kubernetes.io/component: manager
app.kubernetes.io/created-by: k8-operator
app.kubernetes.io/part-of: k8-operator
control-plane: controller-manager
{{- include "secrets-operator.labels" . | nindent 4 }}
spec:
replicas: {{ .Values.controllerManager.replicas }}
selector:
matchLabels:
control-plane: controller-manager
{{- include "secrets-operator.selectorLabels" . | nindent 6 }}
template:
metadata:
labels:
control-plane: controller-manager
{{- include "secrets-operator.selectorLabels" . | nindent 8 }}
annotations:
kubectl.kubernetes.io/default-container: manager
spec:
affinity:
nodeAffinity:
requiredDuringSchedulingIgnoredDuringExecution:
nodeSelectorTerms:
- matchExpressions:
- key: kubernetes.io/arch
operator: In
values:
- amd64
- arm64
- ppc64le
- s390x
- key: kubernetes.io/os
operator: In
values:
- linux
containers:
- args:
- --secure-listen-address=0.0.0.0:8443
- --upstream=http://127.0.0.1:8080/
- --logtostderr=true
- --v=0
env:
- name: KUBERNETES_CLUSTER_DOMAIN
value: {{ .Values.kubernetesClusterDomain }}
image: {{ .Values.controllerManager.kubeRbacProxy.image.repository }}:{{ .Values.controllerManager.kubeRbacProxy.image.tag
| default .Chart.AppVersion }}
name: kube-rbac-proxy
ports:
- containerPort: 8443
name: https
protocol: TCP
resources: {{- toYaml .Values.controllerManager.kubeRbacProxy.resources | nindent
10 }}
securityContext:
allowPrivilegeEscalation: false
capabilities:
drop:
- ALL
- args:
- --health-probe-bind-address=:8081
- --metrics-bind-address=127.0.0.1:8080
- --leader-elect
command:
- /manager
env:
- name: KUBERNETES_CLUSTER_DOMAIN
value: {{ .Values.kubernetesClusterDomain }}
image: {{ .Values.controllerManager.manager.image.repository }}:{{ .Values.controllerManager.manager.image.tag
| default .Chart.AppVersion }}
livenessProbe:
httpGet:
path: /healthz
port: 8081
initialDelaySeconds: 15
periodSeconds: 20
name: manager
readinessProbe:
httpGet:
path: /readyz
port: 8081
initialDelaySeconds: 5
periodSeconds: 10
resources: {{- toYaml .Values.controllerManager.manager.resources | nindent 10
}}
securityContext:
allowPrivilegeEscalation: false
capabilities:
drop:
- ALL
securityContext:
runAsNonRoot: true
serviceAccountName: {{ include "secrets-operator.fullname" . }}-controller-manager
terminationGracePeriodSeconds: 10

View File

@ -0,0 +1,156 @@
apiVersion: apiextensions.k8s.io/v1
kind: CustomResourceDefinition
metadata:
name: infisicalsecrets.secrets.infisical.com
annotations:
controller-gen.kubebuilder.io/version: v0.10.0
labels:
{{- include "secrets-operator.labels" . | nindent 4 }}
spec:
group: secrets.infisical.com
names:
kind: InfisicalSecret
listKind: InfisicalSecretList
plural: infisicalsecrets
singular: infisicalsecret
scope: Namespaced
versions:
- name: v1alpha1
schema:
openAPIV3Schema:
description: InfisicalSecret is the Schema for the infisicalsecrets API
properties:
apiVersion:
description: 'APIVersion defines the versioned schema of this representation
of an object. Servers should convert recognized schemas to the latest
internal value, and may reject unrecognized values. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#resources'
type: string
kind:
description: 'Kind is a string value representing the REST resource this
object represents. Servers may infer this from the endpoint the client
submits requests to. Cannot be updated. In CamelCase. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds'
type: string
metadata:
type: object
spec:
description: InfisicalSecretSpec defines the desired state of InfisicalSecret
properties:
environment:
description: The Infisical environment such as dev, prod, testing
type: string
infisicalToken:
properties:
secretName:
description: The name of the Kubernetes Secret
type: string
secretNamespace:
description: The name space where the Kubernetes Secret is located
type: string
required:
- secretName
- secretNamespace
type: object
managedSecret:
properties:
secretName:
description: The name of the Kubernetes Secret
type: string
secretNamespace:
description: The name space where the Kubernetes Secret is located
type: string
required:
- secretName
- secretNamespace
type: object
projectId:
description: The Infisical project id
type: string
required:
- environment
- projectId
type: object
status:
description: InfisicalSecretStatus defines the observed state of InfisicalSecret
properties:
conditions:
items:
description: "Condition contains details for one aspect of the current
state of this API Resource. --- This struct is intended for direct
use as an array at the field path .status.conditions. For example,
\n type FooStatus struct{ // Represents the observations of a foo's
current state. // Known .status.conditions.type are: \"Available\",
\"Progressing\", and \"Degraded\" // +patchMergeKey=type // +patchStrategy=merge
// +listType=map // +listMapKey=type Conditions []metav1.Condition
`json:\"conditions,omitempty\" patchStrategy:\"merge\" patchMergeKey:\"type\"
protobuf:\"bytes,1,rep,name=conditions\"` \n // other fields }"
properties:
lastTransitionTime:
description: lastTransitionTime is the last time the condition
transitioned from one status to another. This should be when
the underlying condition changed. If that is not known, then
using the time when the API field changed is acceptable.
format: date-time
type: string
message:
description: message is a human readable message indicating details
about the transition. This may be an empty string.
maxLength: 32768
type: string
observedGeneration:
description: observedGeneration represents the .metadata.generation
that the condition was set based upon. For instance, if .metadata.generation
is currently 12, but the .status.conditions[x].observedGeneration
is 9, the condition is out of date with respect to the current
state of the instance.
format: int64
minimum: 0
type: integer
reason:
description: reason contains a programmatic identifier indicating
the reason for the condition's last transition. Producers of
specific condition types may define expected values and meanings
for this field, and whether the values are considered a guaranteed
API. The value should be a CamelCase string. This field may
not be empty.
maxLength: 1024
minLength: 1
pattern: ^[A-Za-z]([A-Za-z0-9_,:]*[A-Za-z0-9_])?$
type: string
status:
description: status of the condition, one of True, False, Unknown.
enum:
- "True"
- "False"
- Unknown
type: string
type:
description: type of condition in CamelCase or in foo.example.com/CamelCase.
--- Many .condition.type values are consistent across resources
like Available, but because arbitrary conditions can be useful
(see .node.status.conditions), the ability to deconflict is
important. The regex it matches is (dns1123SubdomainFmt/)?(qualifiedNameFmt)
maxLength: 316
pattern: ^([a-z0-9]([-a-z0-9]*[a-z0-9])?(\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*/)?(([A-Za-z0-9][-A-Za-z0-9_.]*)?[A-Za-z0-9])$
type: string
required:
- lastTransitionTime
- message
- reason
- status
- type
type: object
type: array
required:
- conditions
type: object
type: object
served: true
storage: true
subresources:
status: {}
status:
acceptedNames:
kind: ""
plural: ""
conditions: []
storedVersions: []

View File

@ -0,0 +1,59 @@
apiVersion: rbac.authorization.k8s.io/v1
kind: Role
metadata:
name: {{ include "secrets-operator.fullname" . }}-leader-election-role
labels:
app.kubernetes.io/component: rbac
app.kubernetes.io/created-by: k8-operator
app.kubernetes.io/part-of: k8-operator
{{- include "secrets-operator.labels" . | nindent 4 }}
rules:
- apiGroups:
- ""
resources:
- configmaps
verbs:
- get
- list
- watch
- create
- update
- patch
- delete
- apiGroups:
- coordination.k8s.io
resources:
- leases
verbs:
- get
- list
- watch
- create
- update
- patch
- delete
- apiGroups:
- ""
resources:
- events
verbs:
- create
- patch
---
apiVersion: rbac.authorization.k8s.io/v1
kind: RoleBinding
metadata:
name: {{ include "secrets-operator.fullname" . }}-leader-election-rolebinding
labels:
app.kubernetes.io/component: rbac
app.kubernetes.io/created-by: k8-operator
app.kubernetes.io/part-of: k8-operator
{{- include "secrets-operator.labels" . | nindent 4 }}
roleRef:
apiGroup: rbac.authorization.k8s.io
kind: Role
name: '{{ include "secrets-operator.fullname" . }}-leader-election-role'
subjects:
- kind: ServiceAccount
name: '{{ include "secrets-operator.fullname" . }}-controller-manager'
namespace: '{{ .Release.Namespace }}'

View File

@ -0,0 +1,71 @@
apiVersion: rbac.authorization.k8s.io/v1
kind: ClusterRole
metadata:
name: {{ include "secrets-operator.fullname" . }}-manager-role
labels:
{{- include "secrets-operator.labels" . | nindent 4 }}
rules:
- apiGroups:
- ""
resources:
- secrets
verbs:
- create
- delete
- get
- list
- update
- watch
- apiGroups:
- apps
resources:
- deployments
verbs:
- get
- list
- update
- watch
- apiGroups:
- secrets.infisical.com
resources:
- infisicalsecrets
verbs:
- create
- delete
- get
- list
- patch
- update
- watch
- apiGroups:
- secrets.infisical.com
resources:
- infisicalsecrets/finalizers
verbs:
- update
- apiGroups:
- secrets.infisical.com
resources:
- infisicalsecrets/status
verbs:
- get
- patch
- update
---
apiVersion: rbac.authorization.k8s.io/v1
kind: ClusterRoleBinding
metadata:
name: {{ include "secrets-operator.fullname" . }}-manager-rolebinding
labels:
app.kubernetes.io/component: rbac
app.kubernetes.io/created-by: k8-operator
app.kubernetes.io/part-of: k8-operator
{{- include "secrets-operator.labels" . | nindent 4 }}
roleRef:
apiGroup: rbac.authorization.k8s.io
kind: ClusterRole
name: '{{ include "secrets-operator.fullname" . }}-manager-role'
subjects:
- kind: ServiceAccount
name: '{{ include "secrets-operator.fullname" . }}-controller-manager'
namespace: '{{ .Release.Namespace }}'

View File

@ -0,0 +1,14 @@
apiVersion: rbac.authorization.k8s.io/v1
kind: ClusterRole
metadata:
name: {{ include "secrets-operator.fullname" . }}-metrics-reader
labels:
app.kubernetes.io/component: kube-rbac-proxy
app.kubernetes.io/created-by: k8-operator
app.kubernetes.io/part-of: k8-operator
{{- include "secrets-operator.labels" . | nindent 4 }}
rules:
- nonResourceURLs:
- /metrics
verbs:
- get

View File

@ -0,0 +1,17 @@
apiVersion: v1
kind: Service
metadata:
name: {{ include "secrets-operator.fullname" . }}-controller-manager-metrics-service
labels:
app.kubernetes.io/component: kube-rbac-proxy
app.kubernetes.io/created-by: k8-operator
app.kubernetes.io/part-of: k8-operator
control-plane: controller-manager
{{- include "secrets-operator.labels" . | nindent 4 }}
spec:
type: {{ .Values.metricsService.type }}
selector:
control-plane: controller-manager
{{- include "secrets-operator.selectorLabels" . | nindent 4 }}
ports:
{{- .Values.metricsService.ports | toYaml | nindent 2 -}}

View File

@ -0,0 +1,40 @@
apiVersion: rbac.authorization.k8s.io/v1
kind: ClusterRole
metadata:
name: {{ include "secrets-operator.fullname" . }}-proxy-role
labels:
app.kubernetes.io/component: kube-rbac-proxy
app.kubernetes.io/created-by: k8-operator
app.kubernetes.io/part-of: k8-operator
{{- include "secrets-operator.labels" . | nindent 4 }}
rules:
- apiGroups:
- authentication.k8s.io
resources:
- tokenreviews
verbs:
- create
- apiGroups:
- authorization.k8s.io
resources:
- subjectaccessreviews
verbs:
- create
---
apiVersion: rbac.authorization.k8s.io/v1
kind: ClusterRoleBinding
metadata:
name: {{ include "secrets-operator.fullname" . }}-proxy-rolebinding
labels:
app.kubernetes.io/component: kube-rbac-proxy
app.kubernetes.io/created-by: k8-operator
app.kubernetes.io/part-of: k8-operator
{{- include "secrets-operator.labels" . | nindent 4 }}
roleRef:
apiGroup: rbac.authorization.k8s.io
kind: ClusterRole
name: '{{ include "secrets-operator.fullname" . }}-proxy-role'
subjects:
- kind: ServiceAccount
name: '{{ include "secrets-operator.fullname" . }}-controller-manager'
namespace: '{{ .Release.Namespace }}'

View File

@ -0,0 +1,32 @@
controllerManager:
kubeRbacProxy:
image:
repository: gcr.io/kubebuilder/kube-rbac-proxy
tag: v0.13.1
resources:
limits:
cpu: 500m
memory: 128Mi
requests:
cpu: 5m
memory: 64Mi
manager:
image:
repository: infisical/kubernetes-operator
tag: latest
resources:
limits:
cpu: 500m
memory: 128Mi
requests:
cpu: 10m
memory: 64Mi
replicas: 1
kubernetesClusterDomain: cluster.local
metricsService:
ports:
- name: https
port: 8443
protocol: TCP
targetPort: https
type: ClusterIP

View File

@ -15,6 +15,7 @@ RUN go mod download
COPY main.go main.go
COPY api/ api/
COPY controllers/ controllers/
COPY packages/ packages/
# Build
# the GOARCH has not a default value to allow the binary be built according to the host where the command

View File

@ -36,6 +36,11 @@ all: build
help: ## Display this help.
@awk 'BEGIN {FS = ":.*##"; printf "\nUsage:\n make \033[36m<target>\033[0m\n"} /^[a-zA-Z_0-9-]+:.*?##/ { printf " \033[36m%-15s\033[0m %s\n", $$1, $$2 } /^##@/ { printf "\n\033[1m%s\033[0m\n", substr($$0, 5) } ' $(MAKEFILE_LIST)
## Chart
helm-chart:
$(KUSTOMIZE) build config/default | helmify ../helm-charts/secrets-operator
##@ Development
.PHONY: manifests

View File

@ -7,11 +7,11 @@ import (
type KubeSecretReference struct {
// The name of the Kubernetes Secret
// +kubebuilder:validation:Required
Name string `json:"name"`
SecretName string `json:"secretName"`
// The name space where the Kubernetes Secret is located
// +kubebuilder:validation:Required
Namespace string `json:"namespace,omitempty"`
SecretNamespace string `json:"secretNamespace"`
}
// InfisicalSecretSpec defines the desired state of InfisicalSecret
@ -21,11 +21,11 @@ type InfisicalSecretSpec struct {
// The Infisical project id
// +kubebuilder:validation:Required
ProjectId string `json:"projectId,omitempty"`
ProjectId string `json:"projectId"`
// The Infisical environment such as dev, prod, testing
// +kubebuilder:validation:Required
Environment string `json:"environment,omitempty"`
Environment string `json:"environment"`
}
// InfisicalSecretStatus defines the observed state of InfisicalSecret

View File

@ -40,29 +40,34 @@ spec:
type: string
infisicalToken:
properties:
name:
secretName:
description: The name of the Kubernetes Secret
type: string
namespace:
secretNamespace:
description: The name space where the Kubernetes Secret is located
type: string
required:
- name
- secretName
- secretNamespace
type: object
managedSecret:
properties:
name:
secretName:
description: The name of the Kubernetes Secret
type: string
namespace:
secretNamespace:
description: The name space where the Kubernetes Secret is located
type: string
required:
- name
- secretName
- secretNamespace
type: object
projectId:
description: The Infisical project id
type: string
required:
- environment
- projectId
type: object
status:
description: InfisicalSecretStatus defines the observed state of InfisicalSecret

View File

@ -9,4 +9,11 @@ metadata:
app.kubernetes.io/created-by: k8-operator
name: infisicalsecret-sample
spec:
# TODO(user): Add fields here
projectId: 62faf98ae0b05e8529b5da46
environment: dev
infisicalToken:
secretName: service-token
secretNamespace: default
managedSecret:
secretName: managed-secret
secretNamespace: default

View File

@ -54,6 +54,7 @@ func (r *InfisicalSecretReconciler) Reconcile(ctx context.Context, req ctrl.Requ
}
err = r.ReconcileInfisicalSecret(ctx, infisicalSecretCR)
r.SetReadyToSyncSecretsConditions(ctx, &infisicalSecretCR, err)
if err != nil {
log.Error(err, "Unable to reconcile Infisical Secret and will try again")
return ctrl.Result{
@ -61,12 +62,15 @@ func (r *InfisicalSecretReconciler) Reconcile(ctx context.Context, req ctrl.Requ
}, nil
}
return ctrl.Result{}, nil
// Sync again after the specified time
return ctrl.Result{
RequeueAfter: time.Minute,
}, nil
}
// SetupWithManager sets up the controller with the Manager.
func (r *InfisicalSecretReconciler) SetupWithManager(mgr ctrl.Manager) error {
return ctrl.NewControllerManagedBy(mgr).
For(&secretsv1alpha1.InfisicalSecret{}).
For(&secretsv1alpha1.InfisicalSecret{}). // TODO we should also be watching secrets with the name specifed
Complete(r)
}

View File

@ -3,12 +3,14 @@ package controllers
import (
"context"
"fmt"
"strings"
"github.com/Infisical/infisical/k8-operator/api/v1alpha1"
api "github.com/Infisical/infisical/k8-operator/packages/api"
models "github.com/Infisical/infisical/k8-operator/packages/models"
corev1 "k8s.io/api/core/v1"
"k8s.io/apimachinery/pkg/api/errors"
"k8s.io/apimachinery/pkg/api/meta"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/types"
)
@ -27,20 +29,20 @@ func (r *InfisicalSecretReconciler) GetKubeSecretByNamespacedName(ctx context.Co
func (r *InfisicalSecretReconciler) GetInfisicalToken(ctx context.Context, infisicalSecret v1alpha1.InfisicalSecret) (string, error) {
tokenSecret, err := r.GetKubeSecretByNamespacedName(ctx, types.NamespacedName{
Namespace: infisicalSecret.Spec.ManagedSecret.Namespace,
Name: infisicalSecret.Spec.ManagedSecret.Name,
Namespace: infisicalSecret.Spec.InfisicalToken.SecretNamespace,
Name: infisicalSecret.Spec.InfisicalToken.SecretName,
})
if err != nil {
return "", fmt.Errorf("failed to read infisical token secret from secret named [%s] in namespace [%s]: with error [%w]", infisicalSecret.Spec.ManagedSecret.Name, infisicalSecret.Spec.ManagedSecret.Namespace, err)
return "", fmt.Errorf("failed to read Infisical token secret from secret named [%s] in namespace [%s]: with error [%w]", infisicalSecret.Spec.ManagedSecret.SecretName, infisicalSecret.Spec.ManagedSecret.SecretNamespace, err)
}
infisicalServiceToken := tokenSecret.Data[INFISICAL_TOKEN_SECRET_KEY_NAME]
if infisicalServiceToken == nil {
return "", fmt.Errorf("the Infisical token is not set in the Kubernetes secret. Please add the key [%s] with the corresponding token value.", INFISICAL_TOKEN_SECRET_KEY_NAME)
return "", fmt.Errorf("the Infisical token is not set in the Kubernetes secret. Please add the key [%s] with the corresponding token value", INFISICAL_TOKEN_SECRET_KEY_NAME)
}
return string(infisicalServiceToken), nil
return strings.Replace(string(infisicalServiceToken), " ", "", -1), nil
}
func (r *InfisicalSecretReconciler) CreateInfisicalManagedKubeSecret(ctx context.Context, infisicalSecret v1alpha1.InfisicalSecret, secretsFromAPI []models.SingleEnvironmentVariable) error {
@ -52,8 +54,8 @@ func (r *InfisicalSecretReconciler) CreateInfisicalManagedKubeSecret(ctx context
// create a new secret as specified by the managed secret spec of CRD
newKubeSecretInstance := &corev1.Secret{
ObjectMeta: metav1.ObjectMeta{
Name: infisicalSecret.Spec.ManagedSecret.Name,
Namespace: infisicalSecret.Spec.ManagedSecret.Namespace,
Name: infisicalSecret.Spec.ManagedSecret.SecretName,
Namespace: infisicalSecret.Spec.ManagedSecret.SecretNamespace,
},
Type: "Opaque",
Data: plainProcessedSecrets,
@ -86,13 +88,14 @@ func (r *InfisicalSecretReconciler) UpdateInfisicalManagedKubeSecret(ctx context
func (r *InfisicalSecretReconciler) ReconcileInfisicalSecret(ctx context.Context, infisicalSecret v1alpha1.InfisicalSecret) error {
infisicalToken, err := r.GetInfisicalToken(ctx, infisicalSecret)
r.SetInfisicalTokenLoadCondition(ctx, &infisicalSecret, err)
if err != nil {
return fmt.Errorf("unable to load Infisical Token from the specified Kubernetes secret with error [%w]", err)
}
managedKubeSecret, err := r.GetKubeSecretByNamespacedName(ctx, types.NamespacedName{
Name: infisicalSecret.Spec.ManagedSecret.Name,
Namespace: infisicalSecret.Spec.ManagedSecret.Namespace,
Name: infisicalSecret.Spec.ManagedSecret.SecretName,
Namespace: infisicalSecret.Spec.ManagedSecret.SecretNamespace,
})
if err != nil && !errors.IsNotFound(err) {
@ -100,6 +103,7 @@ func (r *InfisicalSecretReconciler) ReconcileInfisicalSecret(ctx context.Context
}
secretsFromApi, err := api.GetAllEnvironmentVariables(infisicalSecret.Spec.ProjectId, infisicalSecret.Spec.Environment, infisicalToken)
if err != nil {
return err
}
@ -111,3 +115,59 @@ func (r *InfisicalSecretReconciler) ReconcileInfisicalSecret(ctx context.Context
}
}
// Conditions
func (r *InfisicalSecretReconciler) SetReadyToSyncSecretsConditions(ctx context.Context, infisicalSecret *v1alpha1.InfisicalSecret, errorToConditionOn error) {
if infisicalSecret.Status.Conditions == nil {
infisicalSecret.Status.Conditions = []metav1.Condition{}
}
if errorToConditionOn == nil {
meta.SetStatusCondition(&infisicalSecret.Status.Conditions, metav1.Condition{
Type: "secrets.infisical.com/ReadyToSyncSecrets",
Status: metav1.ConditionTrue,
Reason: "OK",
Message: "Infisical controller has started syncing your secrets",
})
} else {
meta.SetStatusCondition(&infisicalSecret.Status.Conditions, metav1.Condition{
Type: "secrets.infisical.com/ReadyToSyncSecrets",
Status: metav1.ConditionFalse,
Reason: "Error",
Message: fmt.Sprintf("Failed to update secret because: %v", errorToConditionOn),
})
}
err := r.Client.Status().Update(ctx, infisicalSecret)
if err != nil {
fmt.Println("Could not set condition", err)
}
}
func (r *InfisicalSecretReconciler) SetInfisicalTokenLoadCondition(ctx context.Context, infisicalSecret *v1alpha1.InfisicalSecret, errorToConditionOn error) {
if infisicalSecret.Status.Conditions == nil {
infisicalSecret.Status.Conditions = []metav1.Condition{}
}
if errorToConditionOn == nil {
meta.SetStatusCondition(&infisicalSecret.Status.Conditions, metav1.Condition{
Type: "secrets.infisical.com/LoadedInfisicalToken",
Status: metav1.ConditionTrue,
Reason: "OK",
Message: "Infisical controller has located the Infisical token in provided Kubernetes secret",
})
} else {
meta.SetStatusCondition(&infisicalSecret.Status.Conditions, metav1.Condition{
Type: "secrets.infisical.com/LoadedInfisicalToken",
Status: metav1.ConditionFalse,
Reason: "Error",
Message: fmt.Sprintf("Failed to load Infisical Token because: %v", errorToConditionOn),
})
}
err := r.Client.Status().Update(ctx, infisicalSecret)
if err != nil {
fmt.Println("Could not set condition for LoadedInfisicalToken")
}
}

View File

@ -70,7 +70,7 @@ require (
gopkg.in/inf.v0 v0.9.1 // indirect
gopkg.in/yaml.v2 v2.4.0 // indirect
gopkg.in/yaml.v3 v3.0.1 // indirect
k8s.io/api v0.25.0 // indirect
k8s.io/api v0.25.0
k8s.io/apiextensions-apiserver v0.25.0 // indirect
k8s.io/component-base v0.25.0 // indirect
k8s.io/klog/v2 v2.70.1 // indirect

View File

@ -4,6 +4,7 @@ import (
"encoding/base64"
"errors"
"fmt"
"regexp"
"strings"
"github.com/Infisical/infisical/k8-operator/packages/crypto"
@ -20,7 +21,7 @@ func GetAllEnvironmentVariables(projectId string, envName string, infisicalToken
return nil, err
}
return envsFromApi, nil
return SubstituteSecrets(envsFromApi), nil
}
func GetSecretsFromAPIUsingInfisicalToken(infisicalToken string, envName string, projectId string) ([]models.SingleEnvironmentVariable, error) {
@ -106,3 +107,73 @@ func GetSecretsFromAPIUsingInfisicalToken(infisicalToken string, envName string,
return listOfEnv, nil
}
func getExpandedEnvVariable(secrets []models.SingleEnvironmentVariable, variableWeAreLookingFor string, hashMapOfCompleteVariables map[string]string, hashMapOfSelfRefs map[string]string) string {
if value, found := hashMapOfCompleteVariables[variableWeAreLookingFor]; found {
return value
}
for _, secret := range secrets {
if secret.Key == variableWeAreLookingFor {
regex := regexp.MustCompile(`\${([^\}]*)}`)
variablesToPopulate := regex.FindAllString(secret.Value, -1)
// case: variable is a constant so return its value
if len(variablesToPopulate) == 0 {
return secret.Value
}
valueToEdit := secret.Value
for _, variableWithSign := range variablesToPopulate {
variableWithoutSign := strings.Trim(variableWithSign, "}")
variableWithoutSign = strings.Trim(variableWithoutSign, "${")
// case: reference to self
if variableWithoutSign == secret.Key {
hashMapOfSelfRefs[variableWithoutSign] = variableWithoutSign
continue
} else {
var expandedVariableValue string
if preComputedVariable, found := hashMapOfCompleteVariables[variableWithoutSign]; found {
expandedVariableValue = preComputedVariable
} else {
expandedVariableValue = getExpandedEnvVariable(secrets, variableWithoutSign, hashMapOfCompleteVariables, hashMapOfSelfRefs)
hashMapOfCompleteVariables[variableWithoutSign] = expandedVariableValue
}
// If after expanding all the vars above, is the current var a self ref? if so no replacement needed for it
if _, found := hashMapOfSelfRefs[variableWithoutSign]; found {
continue
} else {
valueToEdit = strings.ReplaceAll(valueToEdit, variableWithSign, expandedVariableValue)
}
}
}
return valueToEdit
} else {
continue
}
}
return "${" + variableWeAreLookingFor + "}"
}
func SubstituteSecrets(secrets []models.SingleEnvironmentVariable) []models.SingleEnvironmentVariable {
hashMapOfCompleteVariables := make(map[string]string)
hashMapOfSelfRefs := make(map[string]string)
expandedSecrets := []models.SingleEnvironmentVariable{}
for _, secret := range secrets {
expandedVariable := getExpandedEnvVariable(secrets, secret.Key, hashMapOfCompleteVariables, hashMapOfSelfRefs)
expandedSecrets = append(expandedSecrets, models.SingleEnvironmentVariable{
Key: secret.Key,
Value: expandedVariable,
})
}
return expandedSecrets
}