Files
fission-console/terraform/provider/internal/client/client.go
T
“Naeel” df16b40a9d feat: weather-demo MQ pipeline + sqs-consumer v1.2 with JWT auto-refresh
- sqs-consumer: new Go binary v1.2 with tokenManager (auto-login /auth/login, 5min cache, retry on 401)
- console: add MQ/KW triggers UI (mqtriggers.go, kwtriggers.go, mq.js)
- console: update python-env image to v1.1 (boto3+psycopg2+requests)
- weather-demo: fix consumer/main.py (Flask Request.get_json instead of dict access)
- weather-demo: fix fetcher/main.py (boto3 SQS publish, 5 cities)
- weather-demo: update main.tf (python-env v1.1, deploy_type=literal)
- python-env: add psycopg2-binary to Dockerfile (v1.1)
- terraform provider: client auth fix
2026-05-12 11:19:30 +04:00

520 lines
18 KiB
Go
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
package client
import (
"context"
"fmt"
appsv1 "k8s.io/api/apps/v1"
corev1 "k8s.io/api/core/v1"
apierrors "k8s.io/apimachinery/pkg/api/errors"
k8sresource "k8s.io/apimachinery/pkg/api/resource"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
"k8s.io/apimachinery/pkg/runtime/schema"
"k8s.io/client-go/dynamic"
"k8s.io/client-go/kubernetes"
"k8s.io/client-go/rest"
"k8s.io/client-go/tools/clientcmd"
)
var environmentGVR = schema.GroupVersionResource{
Group: "fission.io",
Version: "v1",
Resource: "environments",
}
var packageGVR = schema.GroupVersionResource{
Group: "fission.io",
Version: "v1",
Resource: "packages",
}
var functionGVR = schema.GroupVersionResource{
Group: "fission.io",
Version: "v1",
Resource: "functions",
}
var httpTriggerGVR = schema.GroupVersionResource{
Group: "fission.io",
Version: "v1",
Resource: "httptriggers",
}
var timeTriggerGVR = schema.GroupVersionResource{
Group: "fission.io",
Version: "v1",
Resource: "timetriggers",
}
var iotDeviceGVR = schema.GroupVersionResource{
Group: "iot.kube5s.ru",
Version: "v1alpha1",
Resource: "iotdevices",
}
const (
sqsConsumerImage = "naeel/sqs-consumer:v1.0"
sqsDefaultEndpoint = "http://shared-sqs.shared-sqs.svc.cluster.local:4100"
fissionRouterBase = "http://router.fission.svc.cluster.local"
mqManagedByLabel = "app.kubernetes.io/managed-by"
mqManagedByVal = "fission-terraform"
mqComponentLabel = "component"
mqComponentVal = "mq-trigger"
)
// Client хранит клиентов Kubernetes API для работы с CRD Fission.
type Client struct {
DynClient dynamic.Interface
K8sClient kubernetes.Interface
Namespace string
}
// New создает Kubernetes clients из kubeconfig/context.
func New(kubeconfigPath, kubeContext, namespace string) (*Client, error) {
cfg, err := buildConfig(kubeconfigPath, kubeContext)
if err != nil {
return nil, err
}
dynClient, err := dynamic.NewForConfig(cfg)
if err != nil {
return nil, fmt.Errorf("create dynamic client: %w", err)
}
k8sClient, err := kubernetes.NewForConfig(cfg)
if err != nil {
return nil, fmt.Errorf("create kubernetes client: %w", err)
}
return &Client{
DynClient: dynClient,
K8sClient: k8sClient,
Namespace: namespace,
}, nil
}
func buildConfig(kubeconfigPath, kubeContext string) (*rest.Config, error) {
loadingRules := &clientcmd.ClientConfigLoadingRules{}
if kubeconfigPath != "" {
loadingRules.ExplicitPath = kubeconfigPath
}
overrides := &clientcmd.ConfigOverrides{}
if kubeContext != "" {
overrides.CurrentContext = kubeContext
}
clientCfg := clientcmd.NewNonInteractiveDeferredLoadingClientConfig(loadingRules, overrides)
cfg, err := clientCfg.ClientConfig()
if err != nil {
return nil, fmt.Errorf("build kube config: %w", err)
}
return cfg, nil
}
// CreateEnvironment создает объект Environment в заданном namespace.
func (c *Client) CreateEnvironment(ctx context.Context, environment *unstructured.Unstructured) (*unstructured.Unstructured, error) {
createdEnvironment, err := c.DynClient.Resource(environmentGVR).Namespace(environment.GetNamespace()).Create(ctx, environment, metav1.CreateOptions{})
if err != nil {
return nil, fmt.Errorf("create environment %q: %w", environment.GetName(), err)
}
return createdEnvironment, nil
}
// GetEnvironment получает объект Environment по имени.
func (c *Client) GetEnvironment(ctx context.Context, namespace, name string) (*unstructured.Unstructured, error) {
environment, err := c.DynClient.Resource(environmentGVR).Namespace(namespace).Get(ctx, name, metav1.GetOptions{})
if err != nil {
return nil, fmt.Errorf("get environment %q: %w", name, err)
}
return environment, nil
}
// UpdateEnvironment обновляет существующий объект Environment.
func (c *Client) UpdateEnvironment(ctx context.Context, environment *unstructured.Unstructured) (*unstructured.Unstructured, error) {
updatedEnvironment, err := c.DynClient.Resource(environmentGVR).Namespace(environment.GetNamespace()).Update(ctx, environment, metav1.UpdateOptions{})
if err != nil {
return nil, fmt.Errorf("update environment %q: %w", environment.GetName(), err)
}
return updatedEnvironment, nil
}
// DeleteEnvironment удаляет объект Environment по имени.
func (c *Client) DeleteEnvironment(ctx context.Context, namespace, name string) error {
err := c.DynClient.Resource(environmentGVR).Namespace(namespace).Delete(ctx, name, metav1.DeleteOptions{})
if err != nil {
return fmt.Errorf("delete environment %q: %w", name, err)
}
return nil
}
// IsNotFound возвращает true для ошибок Kubernetes типа NotFound.
func IsNotFound(err error) bool {
return apierrors.IsNotFound(err)
}
// CreatePackage создает объект Package в заданном namespace.
func (c *Client) CreatePackage(ctx context.Context, pkg *unstructured.Unstructured) (*unstructured.Unstructured, error) {
createdPackage, err := c.DynClient.Resource(packageGVR).Namespace(pkg.GetNamespace()).Create(ctx, pkg, metav1.CreateOptions{})
if err != nil {
return nil, fmt.Errorf("create package %q: %w", pkg.GetName(), err)
}
return createdPackage, nil
}
// GetPackage получает объект Package по имени.
func (c *Client) GetPackage(ctx context.Context, namespace, name string) (*unstructured.Unstructured, error) {
pkg, err := c.DynClient.Resource(packageGVR).Namespace(namespace).Get(ctx, name, metav1.GetOptions{})
if err != nil {
return nil, fmt.Errorf("get package %q: %w", name, err)
}
return pkg, nil
}
// UpdatePackage обновляет существующий объект Package.
func (c *Client) UpdatePackage(ctx context.Context, pkg *unstructured.Unstructured) (*unstructured.Unstructured, error) {
updatedPackage, err := c.DynClient.Resource(packageGVR).Namespace(pkg.GetNamespace()).Update(ctx, pkg, metav1.UpdateOptions{})
if err != nil {
return nil, fmt.Errorf("update package %q: %w", pkg.GetName(), err)
}
return updatedPackage, nil
}
// DeletePackage удаляет объект Package по имени.
func (c *Client) DeletePackage(ctx context.Context, namespace, name string) error {
err := c.DynClient.Resource(packageGVR).Namespace(namespace).Delete(ctx, name, metav1.DeleteOptions{})
if err != nil {
return fmt.Errorf("delete package %q: %w", name, err)
}
return nil
}
// CreateFunction создает объект Function в заданном namespace.
func (c *Client) CreateFunction(ctx context.Context, fn *unstructured.Unstructured) (*unstructured.Unstructured, error) {
createdFunction, err := c.DynClient.Resource(functionGVR).Namespace(fn.GetNamespace()).Create(ctx, fn, metav1.CreateOptions{})
if err != nil {
return nil, fmt.Errorf("create function %q: %w", fn.GetName(), err)
}
return createdFunction, nil
}
// GetFunction получает объект Function по имени.
func (c *Client) GetFunction(ctx context.Context, namespace, name string) (*unstructured.Unstructured, error) {
fn, err := c.DynClient.Resource(functionGVR).Namespace(namespace).Get(ctx, name, metav1.GetOptions{})
if err != nil {
return nil, fmt.Errorf("get function %q: %w", name, err)
}
return fn, nil
}
// UpdateFunction обновляет существующий объект Function.
func (c *Client) UpdateFunction(ctx context.Context, fn *unstructured.Unstructured) (*unstructured.Unstructured, error) {
updatedFunction, err := c.DynClient.Resource(functionGVR).Namespace(fn.GetNamespace()).Update(ctx, fn, metav1.UpdateOptions{})
if err != nil {
return nil, fmt.Errorf("update function %q: %w", fn.GetName(), err)
}
return updatedFunction, nil
}
// DeleteFunction удаляет объект Function по имени.
func (c *Client) DeleteFunction(ctx context.Context, namespace, name string) error {
err := c.DynClient.Resource(functionGVR).Namespace(namespace).Delete(ctx, name, metav1.DeleteOptions{})
if err != nil {
return fmt.Errorf("delete function %q: %w", name, err)
}
return nil
}
// CreateHTTPTrigger создает объект HTTPTrigger в заданном namespace.
func (c *Client) CreateHTTPTrigger(ctx context.Context, trigger *unstructured.Unstructured) (*unstructured.Unstructured, error) {
createdTrigger, err := c.DynClient.Resource(httpTriggerGVR).Namespace(trigger.GetNamespace()).Create(ctx, trigger, metav1.CreateOptions{})
if err != nil {
return nil, fmt.Errorf("create http trigger %q: %w", trigger.GetName(), err)
}
return createdTrigger, nil
}
// GetHTTPTrigger получает объект HTTPTrigger по имени.
func (c *Client) GetHTTPTrigger(ctx context.Context, namespace, name string) (*unstructured.Unstructured, error) {
trigger, err := c.DynClient.Resource(httpTriggerGVR).Namespace(namespace).Get(ctx, name, metav1.GetOptions{})
if err != nil {
return nil, fmt.Errorf("get http trigger %q: %w", name, err)
}
return trigger, nil
}
// UpdateHTTPTrigger обновляет существующий объект HTTPTrigger.
func (c *Client) UpdateHTTPTrigger(ctx context.Context, trigger *unstructured.Unstructured) (*unstructured.Unstructured, error) {
updatedTrigger, err := c.DynClient.Resource(httpTriggerGVR).Namespace(trigger.GetNamespace()).Update(ctx, trigger, metav1.UpdateOptions{})
if err != nil {
return nil, fmt.Errorf("update http trigger %q: %w", trigger.GetName(), err)
}
return updatedTrigger, nil
}
// DeleteHTTPTrigger удаляет объект HTTPTrigger по имени.
func (c *Client) DeleteHTTPTrigger(ctx context.Context, namespace, name string) error {
err := c.DynClient.Resource(httpTriggerGVR).Namespace(namespace).Delete(ctx, name, metav1.DeleteOptions{})
if err != nil {
return fmt.Errorf("delete http trigger %q: %w", name, err)
}
return nil
}
// ── MQ Trigger (K8s Deployment + Secret) ────────────────────────────
func mqSecretName(name string) string { return "sqs-mq-" + name }
func mqDeployName(name string) string { return "mq-" + name }
// MQTriggerInfo — данные MQ trigger, считанные из K8s.
type MQTriggerInfo struct {
Function string
Queue string
SQSEndpoint string
DeployUID string
}
// CreateMQTrigger создаёт Secret + Deployment для SQS consumer.
func (c *Client) CreateMQTrigger(ctx context.Context, name, ns, functionName, queue, sqsEndpoint, accessKey, secretKey string) (*MQTriggerInfo, error) {
if sqsEndpoint == "" {
sqsEndpoint = sqsDefaultEndpoint
}
functionURL := fissionRouterBase + "/" + functionName
secretName := mqSecretName(name)
deployName := mqDeployName(name)
labels := map[string]string{
mqManagedByLabel: mqManagedByVal,
mqComponentLabel: mqComponentVal,
"mq-trigger-name": name,
}
secret := &corev1.Secret{
ObjectMeta: metav1.ObjectMeta{Name: secretName, Namespace: ns, Labels: labels},
StringData: map[string]string{
"SQS_ACCESS_KEY": accessKey,
"SQS_SECRET_KEY": secretKey,
"SQS_ENDPOINT": sqsEndpoint,
},
}
if _, err := c.K8sClient.CoreV1().Secrets(ns).Create(ctx, secret, metav1.CreateOptions{}); err != nil {
return nil, fmt.Errorf("create mq secret %q: %w", secretName, err)
}
replicas := int32(1)
deploy := &appsv1.Deployment{
ObjectMeta: metav1.ObjectMeta{
Name: deployName,
Namespace: ns,
Labels: labels,
Annotations: map[string]string{
"fission-terraform/mq-trigger-name": name,
"fission-terraform/function": functionName,
"fission-terraform/queue": queue,
"fission-terraform/sqs-endpoint": sqsEndpoint,
},
},
Spec: appsv1.DeploymentSpec{
Replicas: &replicas,
Selector: &metav1.LabelSelector{MatchLabels: map[string]string{"mq-trigger-name": name}},
Template: corev1.PodTemplateSpec{
ObjectMeta: metav1.ObjectMeta{Labels: map[string]string{
"mq-trigger-name": name,
mqComponentLabel: mqComponentVal,
}},
Spec: corev1.PodSpec{
Containers: []corev1.Container{{
Name: "sqs-consumer",
Image: sqsConsumerImage,
ImagePullPolicy: corev1.PullAlways,
Env: []corev1.EnvVar{
{Name: "SQS_QUEUE_NAME", Value: queue},
{Name: "SQS_REGION", Value: "us-east-1"},
{Name: "FUNCTION_URL", Value: functionURL},
{Name: "POLL_INTERVAL", Value: "5"},
{Name: "MAX_MESSAGES", Value: "1"},
{Name: "MAX_RETRIES", Value: "3"},
},
EnvFrom: []corev1.EnvFromSource{{
SecretRef: &corev1.SecretEnvSource{
LocalObjectReference: corev1.LocalObjectReference{Name: secretName},
},
}},
Resources: corev1.ResourceRequirements{
Limits: corev1.ResourceList{
corev1.ResourceCPU: k8sresource.MustParse("50m"),
corev1.ResourceMemory: k8sresource.MustParse("32Mi"),
},
Requests: corev1.ResourceList{
corev1.ResourceCPU: k8sresource.MustParse("10m"),
corev1.ResourceMemory: k8sresource.MustParse("16Mi"),
},
},
}},
},
},
},
}
created, err := c.K8sClient.AppsV1().Deployments(ns).Create(ctx, deploy, metav1.CreateOptions{})
if err != nil {
_ = c.K8sClient.CoreV1().Secrets(ns).Delete(ctx, secretName, metav1.DeleteOptions{})
return nil, fmt.Errorf("create mq deployment %q: %w", deployName, err)
}
return &MQTriggerInfo{
Function: functionName,
Queue: queue,
SQSEndpoint: sqsEndpoint,
DeployUID: string(created.UID),
}, nil
}
// GetMQTrigger возвращает информацию о MQ trigger по аннотациям Deployment.
func (c *Client) GetMQTrigger(ctx context.Context, ns, name string) (*MQTriggerInfo, error) {
deploy, err := c.K8sClient.AppsV1().Deployments(ns).Get(ctx, mqDeployName(name), metav1.GetOptions{})
if err != nil {
return nil, fmt.Errorf("get mq deployment %q: %w", name, err)
}
ann := deploy.Annotations
return &MQTriggerInfo{
Function: ann["fission-terraform/function"],
Queue: ann["fission-terraform/queue"],
SQSEndpoint: ann["fission-terraform/sqs-endpoint"],
DeployUID: string(deploy.UID),
}, nil
}
// DeleteMQTrigger удаляет Deployment + Secret MQ trigger.
func (c *Client) DeleteMQTrigger(ctx context.Context, ns, name string) error {
dErr := c.K8sClient.AppsV1().Deployments(ns).Delete(ctx, mqDeployName(name), metav1.DeleteOptions{})
sErr := c.K8sClient.CoreV1().Secrets(ns).Delete(ctx, mqSecretName(name), metav1.DeleteOptions{})
if dErr != nil && !apierrors.IsNotFound(dErr) {
return fmt.Errorf("delete mq deployment %q: %w", name, dErr)
}
if sErr != nil && !apierrors.IsNotFound(sErr) {
return fmt.Errorf("delete mq secret %q: %w", name, sErr)
}
return nil
}
// ── TimeTrigger (Fission CRD) ────────────────────────────────────────
// CreateTimeTrigger создаёт объект TimeTrigger (CRON) в заданном namespace.
func (c *Client) CreateTimeTrigger(ctx context.Context, tt *unstructured.Unstructured) (*unstructured.Unstructured, error) {
created, err := c.DynClient.Resource(timeTriggerGVR).Namespace(tt.GetNamespace()).Create(ctx, tt, metav1.CreateOptions{})
if err != nil {
return nil, fmt.Errorf("create time trigger %q: %w", tt.GetName(), err)
}
return created, nil
}
// GetTimeTrigger возвращает объект TimeTrigger по имени.
func (c *Client) GetTimeTrigger(ctx context.Context, namespace, name string) (*unstructured.Unstructured, error) {
tt, err := c.DynClient.Resource(timeTriggerGVR).Namespace(namespace).Get(ctx, name, metav1.GetOptions{})
if err != nil {
return nil, fmt.Errorf("get time trigger %q: %w", name, err)
}
return tt, nil
}
// UpdateTimeTrigger обновляет объект TimeTrigger.
func (c *Client) UpdateTimeTrigger(ctx context.Context, tt *unstructured.Unstructured) (*unstructured.Unstructured, error) {
updated, err := c.DynClient.Resource(timeTriggerGVR).Namespace(tt.GetNamespace()).Update(ctx, tt, metav1.UpdateOptions{})
if err != nil {
return nil, fmt.Errorf("update time trigger %q: %w", tt.GetName(), err)
}
return updated, nil
}
// DeleteTimeTrigger удаляет объект TimeTrigger по имени.
func (c *Client) DeleteTimeTrigger(ctx context.Context, namespace, name string) error {
if err := c.DynClient.Resource(timeTriggerGVR).Namespace(namespace).Delete(ctx, name, metav1.DeleteOptions{}); err != nil {
return fmt.Errorf("delete time trigger %q: %w", name, err)
}
return nil
}
// ── IoTDevice (CRD iot.kube5s.ru/v1alpha1) ──────────────────────────
// IoTDeviceInfo — данные IoTDevice из статуса CRD.
type IoTDeviceInfo struct {
Phase string
MQTTUsername string
SecretName string
TopicPrefix string
}
// CreateIoTDevice создаёт объект IoTDevice.
func (c *Client) CreateIoTDevice(ctx context.Context, name, namespace, deviceID string, metadata map[string]string) (*IoTDeviceInfo, error) {
obj := &unstructured.Unstructured{
Object: map[string]any{
"apiVersion": "iot.kube5s.ru/v1alpha1",
"kind": "IoTDevice",
"metadata": map[string]any{
"name": name,
"namespace": namespace,
},
"spec": map[string]any{
"deviceId": deviceID,
"enabled": true,
"metadata": metadata,
},
},
}
created, err := c.DynClient.Resource(iotDeviceGVR).Namespace(namespace).Create(ctx, obj, metav1.CreateOptions{})
if err != nil {
return nil, fmt.Errorf("create iot device %q: %w", name, err)
}
return extractIoTDeviceInfo(created), nil
}
// GetIoTDevice возвращает IoTDevice по имени.
func (c *Client) GetIoTDevice(ctx context.Context, namespace, name string) (*IoTDeviceInfo, error) {
obj, err := c.DynClient.Resource(iotDeviceGVR).Namespace(namespace).Get(ctx, name, metav1.GetOptions{})
if err != nil {
return nil, fmt.Errorf("get iot device %q: %w", name, err)
}
return extractIoTDeviceInfo(obj), nil
}
// DeleteIoTDevice удаляет IoTDevice по имени.
func (c *Client) DeleteIoTDevice(ctx context.Context, namespace, name string) error {
if err := c.DynClient.Resource(iotDeviceGVR).Namespace(namespace).Delete(ctx, name, metav1.DeleteOptions{}); err != nil {
return fmt.Errorf("delete iot device %q: %w", name, err)
}
return nil
}
func extractIoTDeviceInfo(obj *unstructured.Unstructured) *IoTDeviceInfo {
status, _, _ := unstructured.NestedMap(obj.Object, "status")
phase, _ := status["phase"].(string)
mqttUsername, _ := status["mqttUsername"].(string)
secretName, _ := status["secretName"].(string)
topicPrefix, _ := status["topicPrefix"].(string)
return &IoTDeviceInfo{
Phase: phase,
MQTTUsername: mqttUsername,
SecretName: secretName,
TopicPrefix: topicPrefix,
}
}