Merge branch 'main' of github.com:Infisical/infisical into sid/k8s-operator

This commit is contained in:
sidwebworks
2025-08-12 16:49:27 +05:30
271 changed files with 10389 additions and 6590 deletions

View 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")
}

View 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)
}
}

View 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")
}

View 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
}

View 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
}

View 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
}

View 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
}
}

View 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
}

View 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")
}
}

View 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
}