mirror of
https://github.com/awatertrevi/infisical.git
synced 2026-09-22 13:39:35 +00:00
Merge branch 'main' of github.com:Infisical/infisical into sid/k8s-operator
This commit is contained in:
630
k8-operator/internal/util/auth.go
Normal file
630
k8-operator/internal/util/auth.go
Normal file
@@ -0,0 +1,630 @@
|
||||
package util
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
|
||||
"errors"
|
||||
|
||||
corev1 "k8s.io/api/core/v1"
|
||||
|
||||
authenticationv1 "k8s.io/api/authentication/v1"
|
||||
|
||||
"github.com/Infisical/infisical/k8-operator/api/v1alpha1"
|
||||
"github.com/aws/smithy-go/ptr"
|
||||
infisicalSdk "github.com/infisical/go-sdk"
|
||||
"sigs.k8s.io/controller-runtime/pkg/client"
|
||||
)
|
||||
|
||||
func GetServiceAccountToken(k8sClient client.Client, namespace string, serviceAccountName string, autoCreateServiceAccountToken bool, serviceAccountTokenAudiences []string, isNamespaceScoped bool) (string, error) {
|
||||
|
||||
if autoCreateServiceAccountToken {
|
||||
restClient, err := GetRestClientFromClient()
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("failed to get REST client: %w", err)
|
||||
}
|
||||
|
||||
tokenRequest := &authenticationv1.TokenRequest{
|
||||
Spec: authenticationv1.TokenRequestSpec{
|
||||
ExpirationSeconds: ptr.Int64(600), // 10 minutes. the token only needs to be valid for when we do the initial k8s login.
|
||||
},
|
||||
}
|
||||
|
||||
if len(serviceAccountTokenAudiences) > 0 {
|
||||
// Conditionally add the audiences if they are specified.
|
||||
// Failing to do this causes a default audience to be used, which is not what we want if the user doesn't specify any.
|
||||
tokenRequest.Spec.Audiences = serviceAccountTokenAudiences
|
||||
}
|
||||
|
||||
result := &authenticationv1.TokenRequest{}
|
||||
err = restClient.
|
||||
Post().
|
||||
Namespace(namespace).
|
||||
Resource("serviceaccounts").
|
||||
Name(serviceAccountName).
|
||||
SubResource("token").
|
||||
Body(tokenRequest).
|
||||
Do(context.Background()).
|
||||
Into(result)
|
||||
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("failed to create token: %w", err)
|
||||
}
|
||||
|
||||
return result.Status.Token, nil
|
||||
}
|
||||
|
||||
serviceAccount := &corev1.ServiceAccount{}
|
||||
err := k8sClient.Get(context.TODO(), client.ObjectKey{Name: serviceAccountName, Namespace: namespace}, serviceAccount)
|
||||
if err != nil {
|
||||
if IsNamespaceScopedError(err, isNamespaceScoped) {
|
||||
return "", fmt.Errorf("unable to fetch service account. Your Operator is namespace scoped, and cannot read secrets outside of its namespace. Please ensure the service account is in the same namespace as the operator. [err=%v]", err)
|
||||
}
|
||||
return "", err
|
||||
}
|
||||
|
||||
if len(serviceAccount.Secrets) == 0 {
|
||||
return "", fmt.Errorf("no secrets found for service account %s", serviceAccountName)
|
||||
}
|
||||
|
||||
secretName := serviceAccount.Secrets[0].Name
|
||||
|
||||
secret := &corev1.Secret{}
|
||||
err = k8sClient.Get(context.TODO(), client.ObjectKey{Name: secretName, Namespace: namespace}, secret)
|
||||
if err != nil {
|
||||
if IsNamespaceScopedError(err, isNamespaceScoped) {
|
||||
return "", fmt.Errorf("unable to fetch service account token secret. Your Operator is namespace scoped, and cannot read secrets outside of its namespace. Please ensure the service account token secret is in the same namespace as the operator. [err=%v]", err)
|
||||
}
|
||||
return "", err
|
||||
}
|
||||
|
||||
token := secret.Data["token"]
|
||||
|
||||
return string(token), nil
|
||||
}
|
||||
|
||||
type AuthStrategyType string
|
||||
|
||||
var AuthStrategy = struct {
|
||||
SERVICE_TOKEN AuthStrategyType
|
||||
SERVICE_ACCOUNT AuthStrategyType
|
||||
UNIVERSAL_MACHINE_IDENTITY AuthStrategyType
|
||||
KUBERNETES_MACHINE_IDENTITY AuthStrategyType
|
||||
AWS_IAM_MACHINE_IDENTITY AuthStrategyType
|
||||
AZURE_MACHINE_IDENTITY AuthStrategyType
|
||||
GCP_ID_TOKEN_MACHINE_IDENTITY AuthStrategyType
|
||||
GCP_IAM_MACHINE_IDENTITY AuthStrategyType
|
||||
LDAP_MACHINE_IDENTITY AuthStrategyType
|
||||
}{
|
||||
SERVICE_TOKEN: "SERVICE_TOKEN",
|
||||
SERVICE_ACCOUNT: "SERVICE_ACCOUNT",
|
||||
UNIVERSAL_MACHINE_IDENTITY: "UNIVERSAL_MACHINE_IDENTITY",
|
||||
KUBERNETES_MACHINE_IDENTITY: "KUBERNETES_AUTH_MACHINE_IDENTITY",
|
||||
AWS_IAM_MACHINE_IDENTITY: "AWS_IAM_MACHINE_IDENTITY",
|
||||
AZURE_MACHINE_IDENTITY: "AZURE_MACHINE_IDENTITY",
|
||||
GCP_ID_TOKEN_MACHINE_IDENTITY: "GCP_ID_TOKEN_MACHINE_IDENTITY",
|
||||
GCP_IAM_MACHINE_IDENTITY: "GCP_IAM_MACHINE_IDENTITY",
|
||||
LDAP_MACHINE_IDENTITY: "LDAP_MACHINE_IDENTITY",
|
||||
}
|
||||
|
||||
type SecretCrdType string
|
||||
|
||||
var SecretCrd = struct {
|
||||
INFISICAL_SECRET SecretCrdType
|
||||
INFISICAL_PUSH_SECRET SecretCrdType
|
||||
INFISICAL_DYNAMIC_SECRET SecretCrdType
|
||||
}{
|
||||
INFISICAL_SECRET: "INFISICAL_SECRET",
|
||||
INFISICAL_PUSH_SECRET: "INFISICAL_PUSH_SECRET",
|
||||
INFISICAL_DYNAMIC_SECRET: "INFISICAL_DYNAMIC_SECRET",
|
||||
}
|
||||
|
||||
type SecretAuthInput struct {
|
||||
Secret interface{}
|
||||
Type SecretCrdType
|
||||
}
|
||||
|
||||
type AuthenticationDetails struct {
|
||||
AuthStrategy AuthStrategyType
|
||||
MachineIdentityScope v1alpha1.MachineIdentityScopeInWorkspace // This will only be set if a machine identity auth method is used (e.g. UniversalAuth or KubernetesAuth, etc.)
|
||||
IsMachineIdentityAuth bool
|
||||
SecretType SecretCrdType
|
||||
}
|
||||
|
||||
var ErrAuthNotApplicable = errors.New("authentication not applicable")
|
||||
|
||||
func HandleUniversalAuth(ctx context.Context, reconcilerClient client.Client, secretCrd SecretAuthInput, infisicalClient infisicalSdk.InfisicalClientInterface, isNamespaceScoped bool) (AuthenticationDetails, error) {
|
||||
|
||||
var universalAuthSpec v1alpha1.UniversalAuthDetails
|
||||
|
||||
switch secretCrd.Type {
|
||||
case SecretCrd.INFISICAL_SECRET:
|
||||
infisicalSecret, ok := secretCrd.Secret.(v1alpha1.InfisicalSecret)
|
||||
|
||||
if !ok {
|
||||
return AuthenticationDetails{}, errors.New("unable to cast secret to InfisicalSecret")
|
||||
}
|
||||
universalAuthSpec = infisicalSecret.Spec.Authentication.UniversalAuth
|
||||
case SecretCrd.INFISICAL_PUSH_SECRET:
|
||||
infisicalPushSecret, ok := secretCrd.Secret.(v1alpha1.InfisicalPushSecret)
|
||||
|
||||
if !ok {
|
||||
return AuthenticationDetails{}, errors.New("unable to cast secret to InfisicalPushSecret")
|
||||
}
|
||||
|
||||
universalAuthSpec = v1alpha1.UniversalAuthDetails{
|
||||
CredentialsRef: infisicalPushSecret.Spec.Authentication.UniversalAuth.CredentialsRef,
|
||||
SecretsScope: v1alpha1.MachineIdentityScopeInWorkspace{},
|
||||
}
|
||||
|
||||
case SecretCrd.INFISICAL_DYNAMIC_SECRET:
|
||||
infisicalDynamicSecret, ok := secretCrd.Secret.(v1alpha1.InfisicalDynamicSecret)
|
||||
|
||||
if !ok {
|
||||
return AuthenticationDetails{}, errors.New("unable to cast secret to InfisicalDynamicSecret")
|
||||
}
|
||||
|
||||
universalAuthSpec = v1alpha1.UniversalAuthDetails{
|
||||
CredentialsRef: infisicalDynamicSecret.Spec.Authentication.UniversalAuth.CredentialsRef,
|
||||
SecretsScope: v1alpha1.MachineIdentityScopeInWorkspace{},
|
||||
}
|
||||
}
|
||||
|
||||
if universalAuthSpec.CredentialsRef.SecretName == "" || universalAuthSpec.CredentialsRef.SecretNamespace == "" {
|
||||
return AuthenticationDetails{}, ErrAuthNotApplicable
|
||||
}
|
||||
|
||||
universalAuthKubeSecret, err := GetInfisicalUniversalAuthFromKubeSecret(ctx, reconcilerClient, v1alpha1.KubeSecretReference{
|
||||
SecretNamespace: universalAuthSpec.CredentialsRef.SecretNamespace,
|
||||
SecretName: universalAuthSpec.CredentialsRef.SecretName,
|
||||
}, isNamespaceScoped)
|
||||
|
||||
if err != nil {
|
||||
return AuthenticationDetails{}, fmt.Errorf("ReconcileInfisicalSecret: unable to get machine identity creds from kube secret [err=%s]", err)
|
||||
}
|
||||
|
||||
if universalAuthKubeSecret.ClientId == "" && universalAuthKubeSecret.ClientSecret == "" {
|
||||
return AuthenticationDetails{}, ErrAuthNotApplicable
|
||||
}
|
||||
|
||||
_, err = infisicalClient.Auth().UniversalAuthLogin(universalAuthKubeSecret.ClientId, universalAuthKubeSecret.ClientSecret)
|
||||
if err != nil {
|
||||
return AuthenticationDetails{}, fmt.Errorf("unable to login with machine identity credentials [err=%s]", err)
|
||||
}
|
||||
|
||||
return AuthenticationDetails{
|
||||
AuthStrategy: AuthStrategy.UNIVERSAL_MACHINE_IDENTITY,
|
||||
MachineIdentityScope: universalAuthSpec.SecretsScope,
|
||||
IsMachineIdentityAuth: true,
|
||||
SecretType: secretCrd.Type,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func HandleLdapAuth(ctx context.Context, reconcilerClient client.Client, secretCrd SecretAuthInput, infisicalClient infisicalSdk.InfisicalClientInterface, isNamespaceScoped bool) (AuthenticationDetails, error) {
|
||||
|
||||
var ldapAuthSpec v1alpha1.LdapAuthDetails
|
||||
|
||||
switch secretCrd.Type {
|
||||
case SecretCrd.INFISICAL_SECRET:
|
||||
infisicalSecret, ok := secretCrd.Secret.(v1alpha1.InfisicalSecret)
|
||||
|
||||
if !ok {
|
||||
return AuthenticationDetails{}, errors.New("unable to cast secret to InfisicalSecret")
|
||||
}
|
||||
ldapAuthSpec = infisicalSecret.Spec.Authentication.LdapAuth
|
||||
case SecretCrd.INFISICAL_PUSH_SECRET:
|
||||
infisicalPushSecret, ok := secretCrd.Secret.(v1alpha1.InfisicalPushSecret)
|
||||
|
||||
if !ok {
|
||||
return AuthenticationDetails{}, errors.New("unable to cast secret to InfisicalPushSecret")
|
||||
}
|
||||
|
||||
ldapAuthSpec = v1alpha1.LdapAuthDetails{
|
||||
CredentialsRef: infisicalPushSecret.Spec.Authentication.LdapAuth.CredentialsRef,
|
||||
SecretsScope: v1alpha1.MachineIdentityScopeInWorkspace{},
|
||||
IdentityID: infisicalPushSecret.Spec.Authentication.LdapAuth.IdentityID,
|
||||
}
|
||||
|
||||
case SecretCrd.INFISICAL_DYNAMIC_SECRET:
|
||||
infisicalDynamicSecret, ok := secretCrd.Secret.(v1alpha1.InfisicalDynamicSecret)
|
||||
|
||||
if !ok {
|
||||
return AuthenticationDetails{}, errors.New("unable to cast secret to InfisicalDynamicSecret")
|
||||
}
|
||||
|
||||
ldapAuthSpec = v1alpha1.LdapAuthDetails{
|
||||
CredentialsRef: infisicalDynamicSecret.Spec.Authentication.LdapAuth.CredentialsRef,
|
||||
SecretsScope: v1alpha1.MachineIdentityScopeInWorkspace{},
|
||||
IdentityID: infisicalDynamicSecret.Spec.Authentication.LdapAuth.IdentityID,
|
||||
}
|
||||
}
|
||||
|
||||
if ldapAuthSpec.CredentialsRef.SecretName == "" || ldapAuthSpec.CredentialsRef.SecretNamespace == "" {
|
||||
return AuthenticationDetails{}, ErrAuthNotApplicable
|
||||
}
|
||||
|
||||
ldapAuthKubeSecret, err := GetInfisicalLdapAuthFromKubeSecret(ctx, reconcilerClient, v1alpha1.KubeSecretReference{
|
||||
SecretNamespace: ldapAuthSpec.CredentialsRef.SecretNamespace,
|
||||
SecretName: ldapAuthSpec.CredentialsRef.SecretName,
|
||||
}, isNamespaceScoped)
|
||||
|
||||
if err != nil {
|
||||
return AuthenticationDetails{}, fmt.Errorf("ReconcileInfisicalSecret: unable to get machine identity creds from kube secret [err=%s]", err)
|
||||
}
|
||||
|
||||
if ldapAuthKubeSecret.Username == "" && ldapAuthKubeSecret.Password == "" {
|
||||
return AuthenticationDetails{}, ErrAuthNotApplicable
|
||||
}
|
||||
|
||||
_, err = infisicalClient.Auth().LdapAuthLogin(ldapAuthSpec.IdentityID, ldapAuthKubeSecret.Username, ldapAuthKubeSecret.Password)
|
||||
if err != nil {
|
||||
return AuthenticationDetails{}, fmt.Errorf("unable to login with machine identity credentials [err=%s]", err)
|
||||
}
|
||||
|
||||
return AuthenticationDetails{
|
||||
AuthStrategy: AuthStrategy.LDAP_MACHINE_IDENTITY,
|
||||
MachineIdentityScope: ldapAuthSpec.SecretsScope,
|
||||
IsMachineIdentityAuth: true,
|
||||
SecretType: secretCrd.Type,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func HandleKubernetesAuth(ctx context.Context, reconcilerClient client.Client, secretCrd SecretAuthInput, infisicalClient infisicalSdk.InfisicalClientInterface, isNamespaceScoped bool) (AuthenticationDetails, error) {
|
||||
var kubernetesAuthSpec v1alpha1.KubernetesAuthDetails
|
||||
|
||||
switch secretCrd.Type {
|
||||
case SecretCrd.INFISICAL_SECRET:
|
||||
infisicalSecret, ok := secretCrd.Secret.(v1alpha1.InfisicalSecret)
|
||||
|
||||
if !ok {
|
||||
return AuthenticationDetails{}, errors.New("unable to cast secret to InfisicalSecret")
|
||||
}
|
||||
kubernetesAuthSpec = infisicalSecret.Spec.Authentication.KubernetesAuth
|
||||
case SecretCrd.INFISICAL_PUSH_SECRET:
|
||||
infisicalPushSecret, ok := secretCrd.Secret.(v1alpha1.InfisicalPushSecret)
|
||||
|
||||
if !ok {
|
||||
return AuthenticationDetails{}, errors.New("unable to cast secret to InfisicalPushSecret")
|
||||
}
|
||||
kubernetesAuthSpec = v1alpha1.KubernetesAuthDetails{
|
||||
IdentityID: infisicalPushSecret.Spec.Authentication.KubernetesAuth.IdentityID,
|
||||
ServiceAccountRef: v1alpha1.KubernetesServiceAccountRef{
|
||||
Namespace: infisicalPushSecret.Spec.Authentication.KubernetesAuth.ServiceAccountRef.Namespace,
|
||||
Name: infisicalPushSecret.Spec.Authentication.KubernetesAuth.ServiceAccountRef.Name,
|
||||
},
|
||||
SecretsScope: v1alpha1.MachineIdentityScopeInWorkspace{},
|
||||
AutoCreateServiceAccountToken: infisicalPushSecret.Spec.Authentication.KubernetesAuth.AutoCreateServiceAccountToken,
|
||||
ServiceAccountTokenAudiences: infisicalPushSecret.Spec.Authentication.KubernetesAuth.ServiceAccountTokenAudiences,
|
||||
}
|
||||
|
||||
case SecretCrd.INFISICAL_DYNAMIC_SECRET:
|
||||
infisicalDynamicSecret, ok := secretCrd.Secret.(v1alpha1.InfisicalDynamicSecret)
|
||||
|
||||
if !ok {
|
||||
return AuthenticationDetails{}, errors.New("unable to cast secret to InfisicalDynamicSecret")
|
||||
}
|
||||
|
||||
kubernetesAuthSpec = v1alpha1.KubernetesAuthDetails{
|
||||
IdentityID: infisicalDynamicSecret.Spec.Authentication.KubernetesAuth.IdentityID,
|
||||
ServiceAccountRef: v1alpha1.KubernetesServiceAccountRef{
|
||||
Namespace: infisicalDynamicSecret.Spec.Authentication.KubernetesAuth.ServiceAccountRef.Namespace,
|
||||
Name: infisicalDynamicSecret.Spec.Authentication.KubernetesAuth.ServiceAccountRef.Name,
|
||||
},
|
||||
SecretsScope: v1alpha1.MachineIdentityScopeInWorkspace{},
|
||||
AutoCreateServiceAccountToken: infisicalDynamicSecret.Spec.Authentication.KubernetesAuth.AutoCreateServiceAccountToken,
|
||||
ServiceAccountTokenAudiences: infisicalDynamicSecret.Spec.Authentication.KubernetesAuth.ServiceAccountTokenAudiences,
|
||||
}
|
||||
}
|
||||
|
||||
if kubernetesAuthSpec.IdentityID == "" {
|
||||
return AuthenticationDetails{}, ErrAuthNotApplicable
|
||||
}
|
||||
|
||||
serviceAccountToken, err := GetServiceAccountToken(
|
||||
reconcilerClient,
|
||||
kubernetesAuthSpec.ServiceAccountRef.Namespace,
|
||||
kubernetesAuthSpec.ServiceAccountRef.Name,
|
||||
kubernetesAuthSpec.AutoCreateServiceAccountToken,
|
||||
kubernetesAuthSpec.ServiceAccountTokenAudiences,
|
||||
isNamespaceScoped,
|
||||
)
|
||||
|
||||
if err != nil {
|
||||
return AuthenticationDetails{}, fmt.Errorf("unable to get service account token [err=%s]", err)
|
||||
}
|
||||
|
||||
_, err = infisicalClient.Auth().KubernetesRawServiceAccountTokenLogin(kubernetesAuthSpec.IdentityID, serviceAccountToken)
|
||||
if err != nil {
|
||||
return AuthenticationDetails{}, fmt.Errorf("unable to login with Kubernetes native auth [err=%s]", err)
|
||||
}
|
||||
|
||||
return AuthenticationDetails{
|
||||
AuthStrategy: AuthStrategy.KUBERNETES_MACHINE_IDENTITY,
|
||||
MachineIdentityScope: kubernetesAuthSpec.SecretsScope,
|
||||
IsMachineIdentityAuth: true,
|
||||
SecretType: secretCrd.Type,
|
||||
}, nil
|
||||
|
||||
}
|
||||
|
||||
func HandleAwsIamAuth(ctx context.Context, reconcilerClient client.Client, secretCrd SecretAuthInput, infisicalClient infisicalSdk.InfisicalClientInterface, _ bool) (AuthenticationDetails, error) {
|
||||
awsIamAuthSpec := v1alpha1.AWSIamAuthDetails{}
|
||||
|
||||
switch secretCrd.Type {
|
||||
case SecretCrd.INFISICAL_SECRET:
|
||||
infisicalSecret, ok := secretCrd.Secret.(v1alpha1.InfisicalSecret)
|
||||
|
||||
if !ok {
|
||||
return AuthenticationDetails{}, errors.New("unable to cast secret to InfisicalSecret")
|
||||
}
|
||||
|
||||
awsIamAuthSpec = infisicalSecret.Spec.Authentication.AwsIamAuth
|
||||
case SecretCrd.INFISICAL_PUSH_SECRET:
|
||||
infisicalPushSecret, ok := secretCrd.Secret.(v1alpha1.InfisicalPushSecret)
|
||||
|
||||
if !ok {
|
||||
return AuthenticationDetails{}, errors.New("unable to cast secret to InfisicalPushSecret")
|
||||
}
|
||||
|
||||
awsIamAuthSpec = v1alpha1.AWSIamAuthDetails{
|
||||
IdentityID: infisicalPushSecret.Spec.Authentication.AwsIamAuth.IdentityID,
|
||||
SecretsScope: v1alpha1.MachineIdentityScopeInWorkspace{},
|
||||
}
|
||||
|
||||
case SecretCrd.INFISICAL_DYNAMIC_SECRET:
|
||||
infisicalDynamicSecret, ok := secretCrd.Secret.(v1alpha1.InfisicalDynamicSecret)
|
||||
|
||||
if !ok {
|
||||
return AuthenticationDetails{}, errors.New("unable to cast secret to InfisicalDynamicSecret")
|
||||
}
|
||||
|
||||
awsIamAuthSpec = v1alpha1.AWSIamAuthDetails{
|
||||
IdentityID: infisicalDynamicSecret.Spec.Authentication.AwsIamAuth.IdentityID,
|
||||
SecretsScope: v1alpha1.MachineIdentityScopeInWorkspace{},
|
||||
}
|
||||
}
|
||||
|
||||
if awsIamAuthSpec.IdentityID == "" {
|
||||
return AuthenticationDetails{}, ErrAuthNotApplicable
|
||||
}
|
||||
|
||||
_, err := infisicalClient.Auth().AwsIamAuthLogin(awsIamAuthSpec.IdentityID)
|
||||
if err != nil {
|
||||
return AuthenticationDetails{}, fmt.Errorf("unable to login with AWS IAM auth [err=%s]", err)
|
||||
}
|
||||
|
||||
return AuthenticationDetails{
|
||||
AuthStrategy: AuthStrategy.AWS_IAM_MACHINE_IDENTITY,
|
||||
MachineIdentityScope: awsIamAuthSpec.SecretsScope,
|
||||
IsMachineIdentityAuth: true,
|
||||
SecretType: secretCrd.Type,
|
||||
}, nil
|
||||
|
||||
}
|
||||
|
||||
func HandleAzureAuth(ctx context.Context, reconcilerClient client.Client, secretCrd SecretAuthInput, infisicalClient infisicalSdk.InfisicalClientInterface, _ bool) (AuthenticationDetails, error) {
|
||||
azureAuthSpec := v1alpha1.AzureAuthDetails{}
|
||||
|
||||
switch secretCrd.Type {
|
||||
case SecretCrd.INFISICAL_SECRET:
|
||||
infisicalSecret, ok := secretCrd.Secret.(v1alpha1.InfisicalSecret)
|
||||
|
||||
if !ok {
|
||||
return AuthenticationDetails{}, errors.New("unable to cast secret to InfisicalSecret")
|
||||
}
|
||||
|
||||
azureAuthSpec = infisicalSecret.Spec.Authentication.AzureAuth
|
||||
|
||||
case SecretCrd.INFISICAL_PUSH_SECRET:
|
||||
infisicalPushSecret, ok := secretCrd.Secret.(v1alpha1.InfisicalPushSecret)
|
||||
|
||||
if !ok {
|
||||
return AuthenticationDetails{}, errors.New("unable to cast secret to InfisicalPushSecret")
|
||||
}
|
||||
|
||||
azureAuthSpec = v1alpha1.AzureAuthDetails{
|
||||
IdentityID: infisicalPushSecret.Spec.Authentication.AzureAuth.IdentityID,
|
||||
Resource: infisicalPushSecret.Spec.Authentication.AzureAuth.Resource,
|
||||
SecretsScope: v1alpha1.MachineIdentityScopeInWorkspace{},
|
||||
}
|
||||
|
||||
case SecretCrd.INFISICAL_DYNAMIC_SECRET:
|
||||
infisicalDynamicSecret, ok := secretCrd.Secret.(v1alpha1.InfisicalDynamicSecret)
|
||||
|
||||
if !ok {
|
||||
return AuthenticationDetails{}, errors.New("unable to cast secret to InfisicalDynamicSecret")
|
||||
}
|
||||
|
||||
azureAuthSpec = v1alpha1.AzureAuthDetails{
|
||||
IdentityID: infisicalDynamicSecret.Spec.Authentication.AzureAuth.IdentityID,
|
||||
Resource: infisicalDynamicSecret.Spec.Authentication.AzureAuth.Resource,
|
||||
SecretsScope: v1alpha1.MachineIdentityScopeInWorkspace{},
|
||||
}
|
||||
}
|
||||
|
||||
if azureAuthSpec.IdentityID == "" {
|
||||
return AuthenticationDetails{}, ErrAuthNotApplicable
|
||||
}
|
||||
|
||||
_, err := infisicalClient.Auth().AzureAuthLogin(azureAuthSpec.IdentityID, azureAuthSpec.Resource) // If resource is empty(""), it will default to "https://management.azure.com/" in the SDK.
|
||||
if err != nil {
|
||||
return AuthenticationDetails{}, fmt.Errorf("unable to login with Azure auth [err=%s]", err)
|
||||
}
|
||||
|
||||
return AuthenticationDetails{
|
||||
AuthStrategy: AuthStrategy.AZURE_MACHINE_IDENTITY,
|
||||
MachineIdentityScope: azureAuthSpec.SecretsScope,
|
||||
IsMachineIdentityAuth: true,
|
||||
SecretType: secretCrd.Type,
|
||||
}, nil
|
||||
|
||||
}
|
||||
|
||||
func HandleGcpIdTokenAuth(ctx context.Context, reconcilerClient client.Client, secretCrd SecretAuthInput, infisicalClient infisicalSdk.InfisicalClientInterface, _ bool) (AuthenticationDetails, error) {
|
||||
gcpIdTokenSpec := v1alpha1.GCPIdTokenAuthDetails{}
|
||||
|
||||
switch secretCrd.Type {
|
||||
case SecretCrd.INFISICAL_SECRET:
|
||||
infisicalSecret, ok := secretCrd.Secret.(v1alpha1.InfisicalSecret)
|
||||
|
||||
if !ok {
|
||||
return AuthenticationDetails{}, errors.New("unable to cast secret to InfisicalSecret")
|
||||
}
|
||||
|
||||
gcpIdTokenSpec = infisicalSecret.Spec.Authentication.GcpIdTokenAuth
|
||||
case SecretCrd.INFISICAL_PUSH_SECRET:
|
||||
infisicalPushSecret, ok := secretCrd.Secret.(v1alpha1.InfisicalPushSecret)
|
||||
|
||||
if !ok {
|
||||
return AuthenticationDetails{}, errors.New("unable to cast secret to InfisicalPushSecret")
|
||||
}
|
||||
|
||||
gcpIdTokenSpec = v1alpha1.GCPIdTokenAuthDetails{
|
||||
IdentityID: infisicalPushSecret.Spec.Authentication.GcpIdTokenAuth.IdentityID,
|
||||
SecretsScope: v1alpha1.MachineIdentityScopeInWorkspace{},
|
||||
}
|
||||
|
||||
case SecretCrd.INFISICAL_DYNAMIC_SECRET:
|
||||
infisicalDynamicSecret, ok := secretCrd.Secret.(v1alpha1.InfisicalDynamicSecret)
|
||||
|
||||
if !ok {
|
||||
return AuthenticationDetails{}, errors.New("unable to cast secret to InfisicalDynamicSecret")
|
||||
}
|
||||
|
||||
gcpIdTokenSpec = v1alpha1.GCPIdTokenAuthDetails{
|
||||
IdentityID: infisicalDynamicSecret.Spec.Authentication.GcpIdTokenAuth.IdentityID,
|
||||
SecretsScope: v1alpha1.MachineIdentityScopeInWorkspace{},
|
||||
}
|
||||
}
|
||||
|
||||
if gcpIdTokenSpec.IdentityID == "" {
|
||||
return AuthenticationDetails{}, ErrAuthNotApplicable
|
||||
}
|
||||
|
||||
_, err := infisicalClient.Auth().GcpIdTokenAuthLogin(gcpIdTokenSpec.IdentityID)
|
||||
if err != nil {
|
||||
return AuthenticationDetails{}, fmt.Errorf("unable to login with GCP Id Token auth [err=%s]", err)
|
||||
}
|
||||
|
||||
return AuthenticationDetails{
|
||||
AuthStrategy: AuthStrategy.GCP_ID_TOKEN_MACHINE_IDENTITY,
|
||||
MachineIdentityScope: gcpIdTokenSpec.SecretsScope,
|
||||
IsMachineIdentityAuth: true,
|
||||
SecretType: secretCrd.Type,
|
||||
}, nil
|
||||
|
||||
}
|
||||
|
||||
func HandleGcpIamAuth(ctx context.Context, reconcilerClient client.Client, secretCrd SecretAuthInput, infisicalClient infisicalSdk.InfisicalClientInterface, _ bool) (AuthenticationDetails, error) {
|
||||
gcpIamSpec := v1alpha1.GcpIamAuthDetails{}
|
||||
|
||||
switch secretCrd.Type {
|
||||
case SecretCrd.INFISICAL_SECRET:
|
||||
infisicalSecret, ok := secretCrd.Secret.(v1alpha1.InfisicalSecret)
|
||||
|
||||
if !ok {
|
||||
return AuthenticationDetails{}, errors.New("unable to cast secret to InfisicalSecret")
|
||||
}
|
||||
|
||||
gcpIamSpec = infisicalSecret.Spec.Authentication.GcpIamAuth
|
||||
case SecretCrd.INFISICAL_PUSH_SECRET:
|
||||
infisicalPushSecret, ok := secretCrd.Secret.(v1alpha1.InfisicalPushSecret)
|
||||
|
||||
if !ok {
|
||||
return AuthenticationDetails{}, errors.New("unable to cast secret to InfisicalPushSecret")
|
||||
}
|
||||
|
||||
gcpIamSpec = v1alpha1.GcpIamAuthDetails{
|
||||
IdentityID: infisicalPushSecret.Spec.Authentication.GcpIamAuth.IdentityID,
|
||||
ServiceAccountKeyFilePath: infisicalPushSecret.Spec.Authentication.GcpIamAuth.ServiceAccountKeyFilePath,
|
||||
SecretsScope: v1alpha1.MachineIdentityScopeInWorkspace{},
|
||||
}
|
||||
|
||||
case SecretCrd.INFISICAL_DYNAMIC_SECRET:
|
||||
infisicalDynamicSecret, ok := secretCrd.Secret.(v1alpha1.InfisicalDynamicSecret)
|
||||
|
||||
if !ok {
|
||||
return AuthenticationDetails{}, errors.New("unable to cast secret to InfisicalDynamicSecret")
|
||||
}
|
||||
|
||||
gcpIamSpec = v1alpha1.GcpIamAuthDetails{
|
||||
IdentityID: infisicalDynamicSecret.Spec.Authentication.GcpIamAuth.IdentityID,
|
||||
ServiceAccountKeyFilePath: infisicalDynamicSecret.Spec.Authentication.GcpIamAuth.ServiceAccountKeyFilePath,
|
||||
SecretsScope: v1alpha1.MachineIdentityScopeInWorkspace{},
|
||||
}
|
||||
}
|
||||
|
||||
if gcpIamSpec.IdentityID == "" && gcpIamSpec.ServiceAccountKeyFilePath == "" {
|
||||
return AuthenticationDetails{}, ErrAuthNotApplicable
|
||||
}
|
||||
|
||||
_, err := infisicalClient.Auth().GcpIamAuthLogin(gcpIamSpec.IdentityID, gcpIamSpec.ServiceAccountKeyFilePath)
|
||||
if err != nil {
|
||||
return AuthenticationDetails{}, fmt.Errorf("unable to login with GCP IAM auth [err=%s]", err)
|
||||
}
|
||||
|
||||
return AuthenticationDetails{
|
||||
AuthStrategy: AuthStrategy.GCP_IAM_MACHINE_IDENTITY,
|
||||
MachineIdentityScope: gcpIamSpec.SecretsScope,
|
||||
IsMachineIdentityAuth: true,
|
||||
SecretType: secretCrd.Type,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func HandleAuthentication(ctx context.Context, secretInput SecretAuthInput, reconcilerClient client.Client, infisicalClient infisicalSdk.InfisicalClientInterface, isNamespaceScoped bool) (AuthenticationDetails, error) {
|
||||
|
||||
// We only support legacy auth for InfisicalSecret CRD
|
||||
if secretInput.Type == SecretCrd.INFISICAL_SECRET {
|
||||
infisicalSecret, ok := secretInput.Secret.(v1alpha1.InfisicalSecret)
|
||||
|
||||
if !ok {
|
||||
return AuthenticationDetails{}, errors.New("unable to cast secret to InfisicalSecret")
|
||||
}
|
||||
|
||||
// ? Legacy support, service token auth
|
||||
infisicalToken, err := GetInfisicalTokenFromKubeSecret(ctx, reconcilerClient, infisicalSecret)
|
||||
if err != nil {
|
||||
return AuthenticationDetails{}, fmt.Errorf("ReconcileInfisicalSecret: unable to get service token from kube secret [err=%s]", err)
|
||||
}
|
||||
if infisicalToken != "" {
|
||||
infisicalClient.Auth().SetAccessToken(infisicalToken)
|
||||
return AuthenticationDetails{AuthStrategy: AuthStrategy.SERVICE_TOKEN}, nil
|
||||
}
|
||||
|
||||
// ? Legacy support, service account auth
|
||||
serviceAccountCreds, err := GetInfisicalServiceAccountCredentialsFromKubeSecret(ctx, reconcilerClient, infisicalSecret)
|
||||
if err != nil {
|
||||
return AuthenticationDetails{}, fmt.Errorf("ReconcileInfisicalSecret: unable to get service account creds from kube secret [err=%s]", err)
|
||||
}
|
||||
|
||||
if serviceAccountCreds.AccessKey != "" || serviceAccountCreds.PrivateKey != "" || serviceAccountCreds.PublicKey != "" {
|
||||
infisicalClient.Auth().SetAccessToken(serviceAccountCreds.AccessKey)
|
||||
return AuthenticationDetails{AuthStrategy: AuthStrategy.SERVICE_ACCOUNT}, nil
|
||||
}
|
||||
}
|
||||
|
||||
authStrategies := map[AuthStrategyType]func(ctx context.Context, reconcilerClient client.Client, secretCrd SecretAuthInput, infisicalClient infisicalSdk.InfisicalClientInterface, isNamespaceScoped bool) (AuthenticationDetails, error){
|
||||
AuthStrategy.UNIVERSAL_MACHINE_IDENTITY: HandleUniversalAuth,
|
||||
AuthStrategy.KUBERNETES_MACHINE_IDENTITY: HandleKubernetesAuth,
|
||||
AuthStrategy.AWS_IAM_MACHINE_IDENTITY: HandleAwsIamAuth,
|
||||
AuthStrategy.AZURE_MACHINE_IDENTITY: HandleAzureAuth,
|
||||
AuthStrategy.GCP_ID_TOKEN_MACHINE_IDENTITY: HandleGcpIdTokenAuth,
|
||||
AuthStrategy.GCP_IAM_MACHINE_IDENTITY: HandleGcpIamAuth,
|
||||
AuthStrategy.LDAP_MACHINE_IDENTITY: HandleLdapAuth,
|
||||
}
|
||||
|
||||
for authStrategy, authHandler := range authStrategies {
|
||||
authDetails, err := authHandler(ctx, reconcilerClient, secretInput, infisicalClient, isNamespaceScoped)
|
||||
|
||||
if err == nil {
|
||||
return authDetails, nil
|
||||
}
|
||||
|
||||
if !errors.Is(err, ErrAuthNotApplicable) {
|
||||
return AuthenticationDetails{}, fmt.Errorf("authentication failed for strategy [%s] [err=%w]", authStrategy, err)
|
||||
}
|
||||
}
|
||||
|
||||
return AuthenticationDetails{}, fmt.Errorf("no authentication method provided")
|
||||
|
||||
}
|
||||
59
k8-operator/internal/util/handler.go
Normal file
59
k8-operator/internal/util/handler.go
Normal file
@@ -0,0 +1,59 @@
|
||||
package util
|
||||
|
||||
import (
|
||||
"context"
|
||||
"math/rand"
|
||||
"time"
|
||||
|
||||
"k8s.io/apimachinery/pkg/types"
|
||||
"k8s.io/client-go/util/workqueue"
|
||||
"sigs.k8s.io/controller-runtime/pkg/client"
|
||||
"sigs.k8s.io/controller-runtime/pkg/event"
|
||||
"sigs.k8s.io/controller-runtime/pkg/reconcile"
|
||||
)
|
||||
|
||||
// computeMaxJitterDuration returns a random duration between 0 and max.
|
||||
// This is useful for introducing jitter to event processing.
|
||||
func computeMaxJitterDuration(max time.Duration) (time.Duration, time.Duration) {
|
||||
if max <= 0 {
|
||||
return 0, 0
|
||||
}
|
||||
jitter := time.Duration(rand.Int63n(int64(max)))
|
||||
return max, jitter
|
||||
}
|
||||
|
||||
// EnqueueDelayedEventHandler enqueues reconcile requests with a random delay (jitter)
|
||||
// to spread the load and avoid thundering herd issues.
|
||||
type EnqueueDelayedEventHandler struct {
|
||||
Delay time.Duration
|
||||
}
|
||||
|
||||
func (e *EnqueueDelayedEventHandler) Create(_ context.Context, _ event.TypedCreateEvent[client.Object], _ workqueue.TypedRateLimitingInterface[reconcile.Request]) {
|
||||
}
|
||||
|
||||
func (e *EnqueueDelayedEventHandler) Update(_ context.Context, _ event.TypedUpdateEvent[client.Object], _ workqueue.TypedRateLimitingInterface[reconcile.Request]) {
|
||||
}
|
||||
|
||||
func (e *EnqueueDelayedEventHandler) Delete(_ context.Context, _ event.TypedDeleteEvent[client.Object], _ workqueue.TypedRateLimitingInterface[reconcile.Request]) {
|
||||
}
|
||||
|
||||
func (e *EnqueueDelayedEventHandler) Generic(_ context.Context, evt event.TypedGenericEvent[client.Object], q workqueue.TypedRateLimitingInterface[reconcile.Request]) {
|
||||
if evt.Object == nil {
|
||||
return
|
||||
}
|
||||
|
||||
req := reconcile.Request{
|
||||
NamespacedName: types.NamespacedName{
|
||||
Namespace: evt.Object.GetNamespace(),
|
||||
Name: evt.Object.GetName(),
|
||||
},
|
||||
}
|
||||
|
||||
_, delay := computeMaxJitterDuration(e.Delay)
|
||||
|
||||
if delay > 0 {
|
||||
q.AddAfter(req, delay)
|
||||
} else {
|
||||
q.Add(req)
|
||||
}
|
||||
}
|
||||
60
k8-operator/internal/util/helpers.go
Normal file
60
k8-operator/internal/util/helpers.go
Normal file
@@ -0,0 +1,60 @@
|
||||
package util
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
)
|
||||
|
||||
func ConvertIntervalToDuration(resyncInterval *string) (time.Duration, error) {
|
||||
|
||||
if resyncInterval == nil || *resyncInterval == "" {
|
||||
return 0, nil
|
||||
}
|
||||
|
||||
length := len(*resyncInterval)
|
||||
if length < 2 {
|
||||
return 0, fmt.Errorf("invalid format")
|
||||
}
|
||||
|
||||
unit := (*resyncInterval)[length-1:]
|
||||
numberPart := (*resyncInterval)[:length-1]
|
||||
|
||||
number, err := strconv.Atoi(numberPart)
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
|
||||
switch unit {
|
||||
case "s":
|
||||
if number < 5 {
|
||||
return 0, fmt.Errorf("resync interval must be at least 5 seconds")
|
||||
}
|
||||
return time.Duration(number) * time.Second, nil
|
||||
case "m":
|
||||
return time.Duration(number) * time.Minute, nil
|
||||
case "h":
|
||||
return time.Duration(number) * time.Hour, nil
|
||||
case "d":
|
||||
return time.Duration(number) * 24 * time.Hour, nil
|
||||
case "w":
|
||||
return time.Duration(number) * 7 * 24 * time.Hour, nil
|
||||
default:
|
||||
return 0, fmt.Errorf("invalid time unit")
|
||||
}
|
||||
}
|
||||
|
||||
func AppendAPIEndpoint(address string) string {
|
||||
if strings.HasSuffix(address, "/api") {
|
||||
return address
|
||||
}
|
||||
if address[len(address)-1] == '/' {
|
||||
return address + "api"
|
||||
}
|
||||
return address + "/api"
|
||||
}
|
||||
|
||||
func IsNamespaceScopedError(err error, isNamespaceScoped bool) bool {
|
||||
return isNamespaceScoped && err != nil && strings.Contains(err.Error(), "unknown namespace for the cache")
|
||||
}
|
||||
183
k8-operator/internal/util/kubernetes.go
Normal file
183
k8-operator/internal/util/kubernetes.go
Normal file
@@ -0,0 +1,183 @@
|
||||
package util
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"strings"
|
||||
|
||||
"github.com/Infisical/infisical/k8-operator/api/v1alpha1"
|
||||
"github.com/Infisical/infisical/k8-operator/internal/constants"
|
||||
"github.com/Infisical/infisical/k8-operator/internal/model"
|
||||
corev1 "k8s.io/api/core/v1"
|
||||
k8Errors "k8s.io/apimachinery/pkg/api/errors"
|
||||
"k8s.io/apimachinery/pkg/types"
|
||||
"k8s.io/client-go/kubernetes"
|
||||
"k8s.io/client-go/rest"
|
||||
"k8s.io/client-go/tools/clientcmd"
|
||||
"sigs.k8s.io/controller-runtime/pkg/client"
|
||||
)
|
||||
|
||||
const INFISICAL_MACHINE_IDENTITY_CLIENT_ID = "clientId"
|
||||
const INFISICAL_MACHINE_IDENTITY_CLIENT_SECRET = "clientSecret"
|
||||
|
||||
const INFISICAL_MACHINE_IDENTITY_LDAP_USERNAME = "username"
|
||||
const INFISICAL_MACHINE_IDENTITY_LDAP_PASSWORD = "password"
|
||||
|
||||
func GetKubeSecretByNamespacedName(ctx context.Context, reconcilerClient client.Client, namespacedName types.NamespacedName) (*corev1.Secret, error) {
|
||||
kubeSecret := &corev1.Secret{}
|
||||
err := reconcilerClient.Get(ctx, namespacedName, kubeSecret)
|
||||
if err != nil {
|
||||
kubeSecret = nil
|
||||
}
|
||||
|
||||
return kubeSecret, err
|
||||
}
|
||||
|
||||
func GetKubeConfigMapByNamespacedName(ctx context.Context, reconcilerClient client.Client, namespacedName types.NamespacedName) (*corev1.ConfigMap, error) {
|
||||
kubeConfigMap := &corev1.ConfigMap{}
|
||||
err := reconcilerClient.Get(ctx, namespacedName, kubeConfigMap)
|
||||
if err != nil {
|
||||
kubeConfigMap = nil
|
||||
}
|
||||
|
||||
return kubeConfigMap, err
|
||||
}
|
||||
|
||||
func GetInfisicalUniversalAuthFromKubeSecret(ctx context.Context, reconcilerClient client.Client, universalAuthRef v1alpha1.KubeSecretReference, isNamespaceScoped bool) (machineIdentityDetails model.UniversalAuthIdentityDetails, err error) {
|
||||
|
||||
universalAuthCredsFromKubeSecret, err := GetKubeSecretByNamespacedName(ctx, reconcilerClient, types.NamespacedName{
|
||||
Namespace: universalAuthRef.SecretNamespace,
|
||||
Name: universalAuthRef.SecretName,
|
||||
})
|
||||
|
||||
if k8Errors.IsNotFound(err) {
|
||||
return model.UniversalAuthIdentityDetails{}, nil
|
||||
}
|
||||
|
||||
if err != nil {
|
||||
if IsNamespaceScopedError(err, isNamespaceScoped) {
|
||||
return model.UniversalAuthIdentityDetails{}, fmt.Errorf("unable to fetch Kubernetes secret. Your Operator installation is namespace scoped, and cannot read secrets outside of the namespace it is installed in. Please ensure the secret is in the same namespace as the operator. [err=%v]", err)
|
||||
}
|
||||
return model.UniversalAuthIdentityDetails{}, fmt.Errorf("something went wrong when fetching your machine identity credentials [err=%s]", err)
|
||||
}
|
||||
|
||||
clientIdFromSecret := universalAuthCredsFromKubeSecret.Data[INFISICAL_MACHINE_IDENTITY_CLIENT_ID]
|
||||
clientSecretFromSecret := universalAuthCredsFromKubeSecret.Data[INFISICAL_MACHINE_IDENTITY_CLIENT_SECRET]
|
||||
|
||||
return model.UniversalAuthIdentityDetails{ClientId: string(clientIdFromSecret), ClientSecret: string(clientSecretFromSecret)}, nil
|
||||
|
||||
}
|
||||
|
||||
func GetInfisicalLdapAuthFromKubeSecret(ctx context.Context, reconcilerClient client.Client, ldapAuthRef v1alpha1.KubeSecretReference, isNamespaceScoped bool) (machineIdentityDetails model.LdapIdentityDetails, err error) {
|
||||
|
||||
ldapAuthCredsFromKubeSecret, err := GetKubeSecretByNamespacedName(ctx, reconcilerClient, types.NamespacedName{
|
||||
Namespace: ldapAuthRef.SecretNamespace,
|
||||
Name: ldapAuthRef.SecretName,
|
||||
})
|
||||
|
||||
if k8Errors.IsNotFound(err) {
|
||||
return model.LdapIdentityDetails{}, nil
|
||||
}
|
||||
|
||||
if err != nil {
|
||||
if IsNamespaceScopedError(err, isNamespaceScoped) {
|
||||
return model.LdapIdentityDetails{}, fmt.Errorf("unable to fetch Kubernetes secret. Your Operator is namespace scoped, and cannot read secrets outside of its namespace. Please ensure the secret is in the same namespace as the operator. [err=%v]", err)
|
||||
}
|
||||
return model.LdapIdentityDetails{}, fmt.Errorf("something went wrong when fetching your machine identity credentials [err=%s]", err)
|
||||
}
|
||||
|
||||
usernameFromSecret := ldapAuthCredsFromKubeSecret.Data[INFISICAL_MACHINE_IDENTITY_LDAP_USERNAME]
|
||||
passwordFromSecret := ldapAuthCredsFromKubeSecret.Data[INFISICAL_MACHINE_IDENTITY_LDAP_PASSWORD]
|
||||
|
||||
return model.LdapIdentityDetails{Username: string(usernameFromSecret), Password: string(passwordFromSecret)}, nil
|
||||
|
||||
}
|
||||
|
||||
func getKubeClusterConfig() (*rest.Config, error) {
|
||||
config, err := rest.InClusterConfig()
|
||||
if err != nil {
|
||||
|
||||
loadingRules := clientcmd.NewDefaultClientConfigLoadingRules()
|
||||
configOverrides := &clientcmd.ConfigOverrides{}
|
||||
kubeConfig := clientcmd.NewNonInteractiveDeferredLoadingClientConfig(loadingRules, configOverrides)
|
||||
return kubeConfig.ClientConfig()
|
||||
}
|
||||
|
||||
return config, nil
|
||||
}
|
||||
|
||||
func GetRestClientFromClient() (rest.Interface, error) {
|
||||
|
||||
config, err := getKubeClusterConfig()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
clientset, err := kubernetes.NewForConfig(config)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return clientset.CoreV1().RESTClient(), nil
|
||||
|
||||
}
|
||||
|
||||
func GetInfisicalTokenFromKubeSecret(ctx context.Context, reconcilerClient client.Client, infisicalSecret v1alpha1.InfisicalSecret) (string, error) {
|
||||
// default to new secret ref structure
|
||||
secretName := infisicalSecret.Spec.Authentication.ServiceToken.ServiceTokenSecretReference.SecretName
|
||||
secretNamespace := infisicalSecret.Spec.Authentication.ServiceToken.ServiceTokenSecretReference.SecretNamespace
|
||||
// fall back to previous secret ref
|
||||
if secretName == "" {
|
||||
secretName = infisicalSecret.Spec.TokenSecretReference.SecretName
|
||||
}
|
||||
|
||||
if secretNamespace == "" {
|
||||
secretNamespace = infisicalSecret.Spec.TokenSecretReference.SecretNamespace
|
||||
}
|
||||
|
||||
tokenSecret, err := GetKubeSecretByNamespacedName(ctx, reconcilerClient, types.NamespacedName{
|
||||
Namespace: secretNamespace,
|
||||
Name: secretName,
|
||||
})
|
||||
|
||||
if k8Errors.IsNotFound(err) || (secretNamespace == "" && secretName == "") {
|
||||
return "", nil
|
||||
}
|
||||
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("failed to read Infisical token secret from secret named [%s] in namespace [%s]: with error [%w]", infisicalSecret.Spec.TokenSecretReference.SecretName, infisicalSecret.Spec.TokenSecretReference.SecretNamespace, err)
|
||||
}
|
||||
|
||||
infisicalServiceToken := tokenSecret.Data[constants.INFISICAL_TOKEN_SECRET_KEY_NAME]
|
||||
|
||||
return strings.Replace(string(infisicalServiceToken), " ", "", -1), nil
|
||||
}
|
||||
|
||||
func GetInfisicalServiceAccountCredentialsFromKubeSecret(ctx context.Context, reconcilerClient client.Client, infisicalSecret v1alpha1.InfisicalSecret) (serviceAccountDetails model.ServiceAccountDetails, err error) {
|
||||
|
||||
secretNamespace := infisicalSecret.Spec.Authentication.ServiceAccount.ServiceAccountSecretReference.SecretNamespace
|
||||
secretName := infisicalSecret.Spec.Authentication.ServiceAccount.ServiceAccountSecretReference.SecretName
|
||||
|
||||
serviceAccountCredsFromKubeSecret, err := GetKubeSecretByNamespacedName(ctx, reconcilerClient, types.NamespacedName{
|
||||
Namespace: secretNamespace,
|
||||
Name: secretName,
|
||||
})
|
||||
|
||||
if k8Errors.IsNotFound(err) || (secretNamespace == "" && secretName == "") {
|
||||
return model.ServiceAccountDetails{}, nil
|
||||
}
|
||||
|
||||
if err != nil {
|
||||
return model.ServiceAccountDetails{}, fmt.Errorf("something went wrong when fetching your service account credentials [err=%s]", err)
|
||||
}
|
||||
|
||||
accessKeyFromSecret := serviceAccountCredsFromKubeSecret.Data[constants.SERVICE_ACCOUNT_ACCESS_KEY]
|
||||
publicKeyFromSecret := serviceAccountCredsFromKubeSecret.Data[constants.SERVICE_ACCOUNT_PUBLIC_KEY]
|
||||
privateKeyFromSecret := serviceAccountCredsFromKubeSecret.Data[constants.SERVICE_ACCOUNT_PRIVATE_KEY]
|
||||
|
||||
if accessKeyFromSecret == nil || publicKeyFromSecret == nil || privateKeyFromSecret == nil {
|
||||
return model.ServiceAccountDetails{}, nil
|
||||
}
|
||||
|
||||
return model.ServiceAccountDetails{AccessKey: string(accessKeyFromSecret), PrivateKey: string(privateKeyFromSecret), PublicKey: string(publicKeyFromSecret)}, nil
|
||||
}
|
||||
15
k8-operator/internal/util/models.go
Normal file
15
k8-operator/internal/util/models.go
Normal file
@@ -0,0 +1,15 @@
|
||||
package util
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"github.com/Infisical/infisical/k8-operator/packages/util/sse"
|
||||
infisicalSdk "github.com/infisical/go-sdk"
|
||||
)
|
||||
|
||||
type ResourceVariables struct {
|
||||
InfisicalClient infisicalSdk.InfisicalClientInterface
|
||||
CancelCtx context.CancelFunc
|
||||
AuthDetails AuthenticationDetails
|
||||
ServerSentEvents *sse.ConnectionRegistry
|
||||
}
|
||||
186
k8-operator/internal/util/secrets.go
Normal file
186
k8-operator/internal/util/secrets.go
Normal file
@@ -0,0 +1,186 @@
|
||||
package util
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"strings"
|
||||
|
||||
"github.com/Infisical/infisical/k8-operator/api/v1alpha1"
|
||||
"github.com/Infisical/infisical/k8-operator/internal/api"
|
||||
"github.com/Infisical/infisical/k8-operator/internal/model"
|
||||
"github.com/go-resty/resty/v2"
|
||||
infisical "github.com/infisical/go-sdk"
|
||||
)
|
||||
|
||||
type DecodedSymmetricEncryptionDetails = struct {
|
||||
Cipher []byte
|
||||
IV []byte
|
||||
Tag []byte
|
||||
Key []byte
|
||||
}
|
||||
|
||||
func VerifyServiceToken(serviceToken string) (string, error) {
|
||||
serviceTokenParts := strings.SplitN(serviceToken, ".", 4)
|
||||
if len(serviceTokenParts) < 4 {
|
||||
return "", fmt.Errorf("invalid service token entered. Please double check your service token and try again")
|
||||
}
|
||||
|
||||
serviceToken = fmt.Sprintf("%v.%v.%v", serviceTokenParts[0], serviceTokenParts[1], serviceTokenParts[2])
|
||||
return serviceToken, nil
|
||||
}
|
||||
|
||||
func GetServiceTokenDetails(infisicalToken string) (api.GetServiceTokenDetailsResponse, error) {
|
||||
serviceTokenParts := strings.SplitN(infisicalToken, ".", 4)
|
||||
if len(serviceTokenParts) < 4 {
|
||||
return api.GetServiceTokenDetailsResponse{}, fmt.Errorf("invalid service token entered. Please double check your service token and try again")
|
||||
}
|
||||
|
||||
serviceToken := fmt.Sprintf("%v.%v.%v", serviceTokenParts[0], serviceTokenParts[1], serviceTokenParts[2])
|
||||
|
||||
httpClient := resty.New()
|
||||
httpClient.SetAuthToken(serviceToken).
|
||||
SetHeader("Accept", "application/json")
|
||||
|
||||
serviceTokenDetails, err := api.CallGetServiceTokenDetailsV2(httpClient)
|
||||
if err != nil {
|
||||
return api.GetServiceTokenDetailsResponse{}, fmt.Errorf("unable to get service token details. [err=%v]", err)
|
||||
}
|
||||
|
||||
return serviceTokenDetails, nil
|
||||
}
|
||||
|
||||
func GetPlainTextSecretsViaMachineIdentity(infisicalClient infisical.InfisicalClientInterface, secretScope v1alpha1.MachineIdentityScopeInWorkspace) ([]model.SingleEnvironmentVariable, error) {
|
||||
|
||||
secrets, err := infisicalClient.Secrets().List(infisical.ListSecretsOptions{
|
||||
ProjectSlug: secretScope.ProjectSlug,
|
||||
Environment: secretScope.EnvSlug,
|
||||
Recursive: secretScope.Recursive,
|
||||
SecretPath: secretScope.SecretsPath,
|
||||
IncludeImports: true,
|
||||
ExpandSecretReferences: true,
|
||||
})
|
||||
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("unable to get secrets. [err=%v]", err)
|
||||
}
|
||||
|
||||
var environmentVariables []model.SingleEnvironmentVariable
|
||||
|
||||
for _, secret := range secrets {
|
||||
|
||||
environmentVariables = append(environmentVariables, model.SingleEnvironmentVariable{
|
||||
Key: secret.SecretKey,
|
||||
Value: secret.SecretValue,
|
||||
Type: secret.Type,
|
||||
ID: secret.ID,
|
||||
SecretPath: secret.SecretPath,
|
||||
})
|
||||
}
|
||||
|
||||
return environmentVariables, nil
|
||||
}
|
||||
|
||||
func GetPlainTextSecretsViaServiceToken(infisicalClient infisical.InfisicalClientInterface, fullServiceToken string, envSlug string, secretPath string, recursive bool) ([]model.SingleEnvironmentVariable, error) {
|
||||
serviceTokenParts := strings.SplitN(fullServiceToken, ".", 4)
|
||||
if len(serviceTokenParts) < 4 {
|
||||
return nil, fmt.Errorf("invalid service token entered. Please double check your service token and try again")
|
||||
}
|
||||
|
||||
serviceToken := fmt.Sprintf("%v.%v.%v", serviceTokenParts[0], serviceTokenParts[1], serviceTokenParts[2])
|
||||
|
||||
httpClient := resty.New()
|
||||
|
||||
httpClient.SetAuthToken(serviceToken).
|
||||
SetHeader("Accept", "application/json")
|
||||
|
||||
serviceTokenDetails, err := api.CallGetServiceTokenDetailsV2(httpClient)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("unable to get service token details. [err=%v]", err)
|
||||
}
|
||||
|
||||
secrets, err := infisicalClient.Secrets().List(infisical.ListSecretsOptions{
|
||||
ProjectID: serviceTokenDetails.Workspace,
|
||||
Environment: envSlug,
|
||||
Recursive: recursive,
|
||||
SecretPath: secretPath,
|
||||
IncludeImports: true,
|
||||
ExpandSecretReferences: true,
|
||||
})
|
||||
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
var environmentVariables []model.SingleEnvironmentVariable
|
||||
|
||||
for _, secret := range secrets {
|
||||
|
||||
environmentVariables = append(environmentVariables, model.SingleEnvironmentVariable{
|
||||
Key: secret.SecretKey,
|
||||
Value: secret.SecretValue,
|
||||
Type: secret.Type,
|
||||
ID: secret.ID,
|
||||
SecretPath: secret.SecretPath,
|
||||
})
|
||||
}
|
||||
|
||||
return environmentVariables, nil
|
||||
|
||||
}
|
||||
|
||||
// Fetches plaintext secrets from an API endpoint using a service account.
|
||||
// The function fetches the service account details and keys, decrypts the workspace key, fetches the encrypted secrets for the specified project and environment, and decrypts the secrets using the decrypted workspace key.
|
||||
// Returns the plaintext secrets, encrypted secrets response, and any errors that occurred during the process.
|
||||
func GetPlainTextSecretsViaServiceAccount(infisicalClient infisical.InfisicalClientInterface, serviceAccountCreds model.ServiceAccountDetails, projectId string, environmentName string) ([]model.SingleEnvironmentVariable, error) {
|
||||
httpClient := resty.New()
|
||||
httpClient.SetAuthToken(serviceAccountCreds.AccessKey).
|
||||
SetHeader("Accept", "application/json")
|
||||
|
||||
serviceAccountDetails, err := api.CallGetServiceTokenAccountDetailsV2(httpClient)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("GetPlainTextSecretsViaServiceAccount: unable to get service account details. [err=%v]", err)
|
||||
}
|
||||
|
||||
serviceAccountKeys, err := api.CallGetServiceAccountKeysV2(httpClient, api.GetServiceAccountKeysRequest{ServiceAccountId: serviceAccountDetails.ServiceAccount.ID})
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("GetPlainTextSecretsViaServiceAccount: unable to get service account key details. [err=%v]", err)
|
||||
}
|
||||
|
||||
// find key for requested project
|
||||
var workspaceServiceAccountKey api.ServiceAccountKey
|
||||
for _, serviceAccountKey := range serviceAccountKeys.ServiceAccountKeys {
|
||||
if serviceAccountKey.Workspace == projectId {
|
||||
workspaceServiceAccountKey = serviceAccountKey
|
||||
}
|
||||
}
|
||||
|
||||
if workspaceServiceAccountKey.ID == "" || workspaceServiceAccountKey.EncryptedKey == "" || workspaceServiceAccountKey.Nonce == "" || serviceAccountCreds.PublicKey == "" || serviceAccountCreds.PrivateKey == "" {
|
||||
return nil, fmt.Errorf("unable to find key for [projectId=%s] [err=%v]. Ensure that the given service account has access to given projectId", projectId, err)
|
||||
}
|
||||
|
||||
secrets, err := infisicalClient.Secrets().List(infisical.ListSecretsOptions{
|
||||
ProjectID: projectId,
|
||||
Environment: environmentName,
|
||||
Recursive: false,
|
||||
SecretPath: "/",
|
||||
IncludeImports: true,
|
||||
ExpandSecretReferences: true,
|
||||
})
|
||||
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
var environmentVariables []model.SingleEnvironmentVariable
|
||||
|
||||
for _, secret := range secrets {
|
||||
environmentVariables = append(environmentVariables, model.SingleEnvironmentVariable{
|
||||
Key: secret.SecretKey,
|
||||
Value: secret.SecretValue,
|
||||
Type: secret.Type,
|
||||
ID: secret.ID,
|
||||
SecretPath: secret.SecretPath,
|
||||
})
|
||||
}
|
||||
|
||||
return environmentVariables, nil
|
||||
}
|
||||
116
k8-operator/internal/util/sse/client.go
Normal file
116
k8-operator/internal/util/sse/client.go
Normal file
@@ -0,0 +1,116 @@
|
||||
package sse
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"fmt"
|
||||
"io"
|
||||
"net/http"
|
||||
"strings"
|
||||
"sync"
|
||||
"time"
|
||||
)
|
||||
|
||||
type SSEEvent struct {
|
||||
ID string
|
||||
Event string
|
||||
Data string
|
||||
}
|
||||
|
||||
// SSEClient handles SSE connections
|
||||
type SSEClient struct {
|
||||
URL string
|
||||
Client *http.Client
|
||||
LastHealthCheck time.Time
|
||||
mu *sync.Mutex // for safe concurrent access to LastHealthCheck
|
||||
}
|
||||
|
||||
// NewClient creates a new SSE client
|
||||
func NewClient() SSEClient {
|
||||
return SSEClient{
|
||||
mu: &sync.Mutex{},
|
||||
Client: &http.Client{
|
||||
Timeout: 0, // No timeout for streaming
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
// Connect establishes SSE connection and returns a channel of events
|
||||
func (c *SSEClient) Connect(req *http.Request) (<-chan SSEEvent, <-chan error, error) {
|
||||
// Set required headers for SSE
|
||||
req.Header.Set("Cache-Control", "no-cache")
|
||||
req.Header.Set("Connection", "keep-alive")
|
||||
req.Header.Set("Content-Type", "application/json")
|
||||
|
||||
resp, err := c.Client.Do(req)
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
|
||||
if resp.StatusCode != http.StatusOK {
|
||||
resp.Body.Close()
|
||||
return nil, nil, fmt.Errorf("unexpected status code: %d", resp.StatusCode)
|
||||
}
|
||||
|
||||
eventChan := make(chan SSEEvent)
|
||||
errorChan := make(chan error)
|
||||
|
||||
go c.stream(resp.Body, eventChan, errorChan)
|
||||
|
||||
return eventChan, errorChan, nil
|
||||
}
|
||||
|
||||
func (c *SSEClient) stream(body io.ReadCloser, eventChan chan<- SSEEvent, errorChan chan<- error) {
|
||||
defer body.Close()
|
||||
defer close(eventChan)
|
||||
defer close(errorChan)
|
||||
|
||||
scanner := bufio.NewScanner(body)
|
||||
var event SSEEvent
|
||||
|
||||
for scanner.Scan() {
|
||||
line := strings.TrimSpace(scanner.Text())
|
||||
|
||||
// End of event
|
||||
if line == "" {
|
||||
if event.Data != "" || event.Event != "" {
|
||||
if strings.TrimSpace(event.Data) == "1" {
|
||||
c.mu.Lock()
|
||||
c.LastHealthCheck = time.Now()
|
||||
c.mu.Unlock()
|
||||
} else if event.Event != "ping" {
|
||||
eventChan <- event
|
||||
}
|
||||
|
||||
event = SSEEvent{} // Reset for next event
|
||||
}
|
||||
continue
|
||||
}
|
||||
|
||||
switch {
|
||||
case strings.HasPrefix(line, "data:"):
|
||||
data := strings.TrimSpace(strings.TrimPrefix(line, "data:"))
|
||||
if event.Data != "" {
|
||||
event.Data += "\n"
|
||||
}
|
||||
event.Data += data
|
||||
|
||||
case strings.HasPrefix(line, "event:"):
|
||||
event.Event = strings.TrimSpace(strings.TrimPrefix(line, "event:"))
|
||||
|
||||
case strings.HasPrefix(line, "id:"):
|
||||
event.ID = strings.TrimSpace(strings.TrimPrefix(line, "id:"))
|
||||
|
||||
case strings.HasPrefix(line, "retry:"):
|
||||
// Optional: parse and apply retry interval here
|
||||
|
||||
case strings.HasPrefix(line, ":"):
|
||||
// Comment line — ignored
|
||||
default:
|
||||
// Unknown line format — can log/debug if needed
|
||||
}
|
||||
}
|
||||
|
||||
if err := scanner.Err(); err != nil {
|
||||
errorChan <- err
|
||||
}
|
||||
}
|
||||
165
k8-operator/internal/util/sse/sse.go
Normal file
165
k8-operator/internal/util/sse/sse.go
Normal file
@@ -0,0 +1,165 @@
|
||||
package sse
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"sync"
|
||||
"time"
|
||||
)
|
||||
|
||||
type ConnectionMeta struct {
|
||||
EventChan <-chan SSEEvent
|
||||
ErrorChan <-chan error
|
||||
LastPingAt time.Time
|
||||
Cancel context.CancelFunc
|
||||
}
|
||||
|
||||
type ConnectionRegistry struct {
|
||||
Ctx context.Context
|
||||
meta *ConnectionMeta
|
||||
client SSEClient
|
||||
mu sync.RWMutex
|
||||
|
||||
monitorCancel context.CancelFunc
|
||||
monitorCtx context.Context
|
||||
}
|
||||
|
||||
func NewConnectionRegistry(ctx context.Context) *ConnectionRegistry {
|
||||
monitorCtx, monitorCancel := context.WithCancel(ctx)
|
||||
return &ConnectionRegistry{
|
||||
Ctx: ctx,
|
||||
client: NewClient(),
|
||||
monitorCtx: monitorCtx,
|
||||
monitorCancel: monitorCancel,
|
||||
}
|
||||
}
|
||||
|
||||
// create creates a new connection
|
||||
func (r *ConnectionRegistry) create(req *http.Request) (*ConnectionMeta, error) {
|
||||
// Create new connection using provided request
|
||||
eventChan, errorChan, err := r.client.Connect(req)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to connect: %w", err)
|
||||
}
|
||||
|
||||
meta := &ConnectionMeta{
|
||||
EventChan: eventChan,
|
||||
ErrorChan: errorChan,
|
||||
LastPingAt: time.Now(),
|
||||
}
|
||||
|
||||
r.meta = meta
|
||||
|
||||
// Start cleanup monitor for this connection (NON-BLOCKING)
|
||||
go r.monitor(meta)
|
||||
|
||||
println("Creating new connection\n")
|
||||
return meta, nil
|
||||
}
|
||||
|
||||
// Get retrieves the existing connection
|
||||
func (r *ConnectionRegistry) Get() (*ConnectionMeta, bool) {
|
||||
r.mu.RLock()
|
||||
defer r.mu.RUnlock()
|
||||
return r.meta, r.meta != nil
|
||||
}
|
||||
|
||||
// Close closes the connection
|
||||
func (r *ConnectionRegistry) Close() {
|
||||
r.mu.Lock()
|
||||
defer r.mu.Unlock()
|
||||
|
||||
if r.meta != nil {
|
||||
if r.meta.Cancel != nil {
|
||||
r.meta.Cancel()
|
||||
}
|
||||
r.meta = nil
|
||||
}
|
||||
|
||||
// Cancel the monitor
|
||||
if r.monitorCancel != nil {
|
||||
r.monitorCancel()
|
||||
}
|
||||
}
|
||||
|
||||
// IsConnected returns whether there's an active connection
|
||||
func (r *ConnectionRegistry) IsConnected() bool {
|
||||
r.mu.RLock()
|
||||
defer r.mu.RUnlock()
|
||||
return r.meta != nil
|
||||
}
|
||||
|
||||
// UpdateLastPing updates the last ping time
|
||||
func (r *ConnectionRegistry) UpdateLastPing() {
|
||||
r.mu.Lock()
|
||||
defer r.mu.Unlock()
|
||||
|
||||
if r.meta != nil {
|
||||
r.meta.LastPingAt = time.Now()
|
||||
}
|
||||
}
|
||||
|
||||
// monitor watches for connection closure and cleans up
|
||||
func (r *ConnectionRegistry) monitor(meta *ConnectionMeta) {
|
||||
ticker := time.NewTicker(30 * time.Second)
|
||||
defer ticker.Stop()
|
||||
|
||||
for {
|
||||
select {
|
||||
case <-r.monitorCtx.Done():
|
||||
// Context cancelled, exit monitor
|
||||
return
|
||||
|
||||
case <-ticker.C:
|
||||
r.mu.RLock()
|
||||
currentMeta := r.meta
|
||||
r.mu.RUnlock()
|
||||
|
||||
// Check if this monitor is still relevant
|
||||
if currentMeta != meta {
|
||||
// This connection has been replaced, exit monitor
|
||||
return
|
||||
}
|
||||
|
||||
if currentMeta != nil && time.Since(currentMeta.LastPingAt) > 2*time.Minute {
|
||||
fmt.Println("Last ping was more than 2 minutes ago, closing connection")
|
||||
r.mu.Lock()
|
||||
if r.meta == meta { // Double-check under lock
|
||||
if r.meta.Cancel != nil {
|
||||
r.meta.Cancel()
|
||||
}
|
||||
r.meta = nil
|
||||
}
|
||||
r.mu.Unlock()
|
||||
return // Exit monitor after cleanup
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Subscribe provides a convenient way to get events from the connection
|
||||
func (r *ConnectionRegistry) Subscribe(build func() (*http.Request, error)) (<-chan SSEEvent, <-chan error, error) {
|
||||
r.mu.Lock()
|
||||
defer r.mu.Unlock()
|
||||
|
||||
// Get existing connection if available
|
||||
if r.meta != nil {
|
||||
return r.meta.EventChan, r.meta.ErrorChan, nil
|
||||
}
|
||||
|
||||
req, err := build()
|
||||
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
|
||||
// Create new connection if none exists
|
||||
meta, err := r.create(req)
|
||||
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
|
||||
return meta.EventChan, meta.ErrorChan, nil
|
||||
}
|
||||
40
k8-operator/internal/util/time.go
Normal file
40
k8-operator/internal/util/time.go
Normal file
@@ -0,0 +1,40 @@
|
||||
package util
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"strconv"
|
||||
"time"
|
||||
)
|
||||
|
||||
func ConvertResyncIntervalToDuration(resyncInterval string) (time.Duration, error) {
|
||||
length := len(resyncInterval)
|
||||
if length < 2 {
|
||||
return 0, fmt.Errorf("invalid format")
|
||||
}
|
||||
|
||||
unit := resyncInterval[length-1:]
|
||||
numberPart := resyncInterval[:length-1]
|
||||
|
||||
number, err := strconv.Atoi(numberPart)
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
|
||||
switch unit {
|
||||
case "s":
|
||||
if number < 5 {
|
||||
return 0, fmt.Errorf("resync interval must be at least 5 seconds")
|
||||
}
|
||||
return time.Duration(number) * time.Second, nil
|
||||
case "m":
|
||||
return time.Duration(number) * time.Minute, nil
|
||||
case "h":
|
||||
return time.Duration(number) * time.Hour, nil
|
||||
case "d":
|
||||
return time.Duration(number) * 24 * time.Hour, nil
|
||||
case "w":
|
||||
return time.Duration(number) * 7 * 24 * time.Hour, nil
|
||||
default:
|
||||
return 0, fmt.Errorf("invalid time unit")
|
||||
}
|
||||
}
|
||||
44
k8-operator/internal/util/workspace.go
Normal file
44
k8-operator/internal/util/workspace.go
Normal file
@@ -0,0 +1,44 @@
|
||||
package util
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
"github.com/Infisical/infisical/k8-operator/internal/api"
|
||||
"github.com/Infisical/infisical/k8-operator/internal/model"
|
||||
"github.com/go-resty/resty/v2"
|
||||
)
|
||||
|
||||
func GetProjectByID(accessToken string, projectId string) (model.Project, error) {
|
||||
httpClient := resty.New()
|
||||
httpClient.
|
||||
SetAuthScheme("Bearer").
|
||||
SetAuthToken(accessToken).
|
||||
SetHeader("Accept", "application/json")
|
||||
|
||||
projectDetails, err := api.CallGetProjectByID(httpClient, api.GetProjectByIDRequest{
|
||||
ProjectID: projectId,
|
||||
})
|
||||
if err != nil {
|
||||
return model.Project{}, fmt.Errorf("unable to get project by slug. [err=%v]", err)
|
||||
}
|
||||
|
||||
return projectDetails.Project, nil
|
||||
}
|
||||
|
||||
func GetProjectBySlug(accessToken string, projectSlug string) (model.Project, error) {
|
||||
httpClient := resty.New()
|
||||
httpClient.
|
||||
SetAuthScheme("Bearer").
|
||||
SetAuthToken(accessToken).
|
||||
SetHeader("Accept", "application/json")
|
||||
|
||||
project, err := api.CallGetProjectByIDv2(httpClient, api.GetProjectByIDRequest{
|
||||
ProjectID: projectSlug,
|
||||
})
|
||||
|
||||
if err != nil {
|
||||
return model.Project{}, fmt.Errorf("unable to get project by slug. [err=%v]", err)
|
||||
}
|
||||
|
||||
return project, nil
|
||||
}
|
||||
Reference in New Issue
Block a user