diff --git a/.github/workflows/docker-image.yml b/.github/workflows/docker-image.yml index d1c1a40..fc8f36b 100644 --- a/.github/workflows/docker-image.yml +++ b/.github/workflows/docker-image.yml @@ -41,16 +41,20 @@ jobs: } # Check each service directory for changes - if echo "$CHANGED_FILES" | grep -q "^api-gateway/"; then - build_image "api-gateway" "api-gateway" + if echo "$CHANGED_FILES" | grep -q "^persys-gateway/"; then + build_image "api-gateway" "persys-gateway" fi - if echo "$CHANGED_FILES" | grep -q "^prow/"; then - build_image "prow" "prow" + if echo "$CHANGED_FILES" | grep -q "^persys-scheduler/"; then + build_image "persys-scheduler" "persys-scheduler" fi - if echo "$CHANGED_FILES" | grep -q "^cloud-mgmt/"; then - build_image "cloud-mgmt" "cloud-mgmt" + if echo "$CHANGED_FILES" | grep -q "^persys-federation/"; then + build_image "persys-federation" "persys-federation" + fi + + if echo "$CHANGED_FILES" | grep -q "^persys-forgery/"; then + build_image "persys-forgery" "persys-forgery" fi if echo "$CHANGED_FILES" | grep -q "^persys-cfssl/"; then diff --git a/.github/workflows/go.yml b/.github/workflows/go.yml index 9718f92..a7a74ff 100644 --- a/.github/workflows/go.yml +++ b/.github/workflows/go.yml @@ -22,19 +22,25 @@ jobs: go-version: 1.24 - name: Build API Gateway - working-directory: ./api-gateway + working-directory: ./persys-gateway run: | go mod download cd cmd && go build -o main - - name: Build Prow - working-directory: ./prow + - name: Build Persys Scheduler + working-directory: ./persys-scheduler run: | go mod download cd cmd/scheduler && go build -o main - # - name: Build Cloud Mgmt - # working-directory: ./cloud-mgmt - # run: | - # go mod download - # cd cmd && go build -o main + - name: Build Persys Federation + working-directory: ./persys-federation + run: | + go mod download + cd cmd && go build -o main + + - name: Build Persys Forgery + working-directory: ./persys-forgery + run: | + go mod download + cd cmd && go build -o main \ No newline at end of file diff --git a/.gitmodules b/.gitmodules index d65014d..608d919 100644 --- a/.gitmodules +++ b/.gitmodules @@ -1,6 +1,6 @@ [submodule "persys-operator"] path = persys-operator - url = https://github.com/persys-dev/persys-operator -[submodule "persys-agent"] - path = persys-agent - url = https://github.com/persys-dev/persys-agent + url = https://github.com/persys-dev/persys-operator.git +[submodule "compute-agent"] + path = compute-agent + url = https://github.com/persys-dev/compute-agent.git diff --git a/README.md b/README.md index 3dcad4d..976dfa9 100644 --- a/README.md +++ b/README.md @@ -1,299 +1,318 @@ -# Persys Cloud +# Persys Compute -[![License](https://img.shields.io/badge/License-MIT-blue.svg)](LICENSE) -[![Go Version](https://img.shields.io/github/go-mod/go-version/persys-dev/persys-cloud/api-gateway)](go.mod) -[![Build Status](https://img.shields.io/badge/build-passing-brightgreen)](https://github.com/persys-dev/persys-cloud/actions) +![License](https://img.shields.io/badge/license-MIT-blue.svg) +![Language](https://img.shields.io/badge/language-Go-00ADD8) +![Architecture](https://img.shields.io/badge/architecture-control--plane-orange) +![Transport](https://img.shields.io/badge/transport-gRPC%20%2B%20mTLS-green) -> **Note**: This project is under active development and may have issues and limitations. We welcome contributions and feedback! +Persys Compute is a scheduler-driven distributed compute control plane +designed to orchestrate containers, Docker Compose applications, and +virtual machines across heterogeneous infrastructure. -## Table of Contents +It is built with a clear architectural philosophy: -- [Overview](#overview) -- [Architecture](#architecture) -- [Core Components](#core-components) -- [Quick Start](#quick-start) -- [Local Development](#local-development) -- [Services](#services) -- [Documentation](#documentation) -- [Contributing](#contributing) -- [Community](#community) -- [License](#license) +- Centralized scheduler (authoritative desired state) +- Dumb but reliable agents (local execution only) +- Strongly typed gRPC contracts +- mTLS everywhere +- etcd-backed persistent cluster state +- Explicit reconciliation loops +- Resource-aware placement +- Lease-based node liveness model -## Overview +This repository currently lives under `persys-cloud`, but the product +name is **Persys Compute**. -Persys Cloud is an open-source platform that provides **DevOps/SRE as a Service** with automated workload orchestration across both Kubernetes clusters and Docker-based Linux nodes. It simplifies the deployment and management of containerized applications through intelligent scheduling and multi-cloud infrastructure management. +------------------------------------------------------------------------ -### What is Persys Cloud? +# Vision -Persys Cloud is a comprehensive platform that offers: +Persys Compute is not just a container orchestrator. -- **Hybrid Workload Orchestration**: Manage workloads across Kubernetes clusters and Docker-based Linux nodes -- **Intelligent Scheduling**: Automated workload placement and resource optimization -- **Multi-cloud Infrastructure Management**: Unified management across AWS, Azure, GCP, and on-premises -- **DevOps/SRE Automation**: Streamlined CI/CD pipelines and operational tasks -- **Secure Agent Communication**: mTLS-based secure communication between components -- **Real-time Monitoring**: Container status monitoring and health checks +It is a programmable compute control plane designed to: -## Architecture +- Run Docker containers +- Run Docker Compose stacks (from Git or inline spec) +- Provision and manage virtual machines +- Enforce CPU, memory, disk limits +- Reconcile drift automatically +- Scale from a single bare-metal server to multi-node clusters +- Support hybrid and federated deployments -### System Design +The long-term goal is hyperscaler-style infrastructure for private and +hybrid environments --- without unnecessary abstraction or +overengineering. -Persys Cloud uses a distributed architecture with secure agent-based communication: +------------------------------------------------------------------------ -![System Architecture](docs/architecture/persys-cloud.drawio.png) +# Core Architecture -### Core Architecture Components +Persys Compute follows a strict control-plane model. -1. **API Gateway**: Main entry point for all API requests with authentication and routing -2. **Prow Scheduler**: Intelligent workload scheduler that manages container deployment -3. **Persys Agent**: Lightweight agent deployed on Docker-based Linux nodes for workload execution -4. **Cloud Management**: Multi-cloud infrastructure provisioning and management -5. **Persys CFSSL**: Certificate authority for secure mTLS communication -6. **ETCD**: Distributed key-value store for configuration and state management -7. **CoreDNS**: Service discovery and DNS management +## Northbound (User → Control Plane) -### Request Flow +User → Gateway (HTTP + mTLS) → Scheduler -Here's how requests flow through the system: +- REST APIs for platform access +- Authentication & service identity +- Workload submission +- Administrative operations -![Request Flow](docs/architecture/arch-flow.drawio.png) +## Southbound (Control Plane → Nodes) -> **Have suggestions or found issues?** Please [open an issue](https://github.com/persys-dev/persys-cloud/issues) with your feedback! +Scheduler ⇄ Agent (gRPC + mTLS) -## Core Components +- Node registration +- Heartbeat & lease model +- Workload lifecycle execution +- Status reporting -### Prow Scheduler -The intelligent workload orchestrator that: -- Manages workload scheduling across nodes -- Handles container lifecycle management -- Provides real-time monitoring and status updates -- Implements secure mTLS communication with agents -- Stores configuration and state in ETCD +Agents never schedule themselves. Scheduler owns all placement +decisions. -### Persys Agent -Lightweight agents deployed on Docker-based Linux nodes that: -- Execute container workloads (Docker containers, Docker Compose, Git-based deployments) -- Report node status and resource utilization -- Handle secure communication with the scheduler -- Manage local container lifecycle -- Support multiple workload types: - - `docker-container`: Single container deployments - - `docker-compose`: Multi-container applications - - `git-compose`: Git repository-based deployments +------------------------------------------------------------------------ -### Cloud Management -Multi-cloud infrastructure management service that: -- Provisions and manages Kubernetes clusters -- Handles cloud provider integrations (AWS, Azure, GCP) -- Manages infrastructure as code with Terraform -- Provides unified cloud resource management +# Control Plane Components -### API Gateway -Centralized API management that: -- Provides RESTful APIs for all platform operations -- Handles authentication and authorization -- Routes requests to appropriate services -- Manages API versioning and documentation +## persys-scheduler -## Quick Start +Authoritative cluster brain. -### Prerequisites +Responsibilities: -- Docker and Docker Compose -- Kubernetes CLI (kubectl) - for Kubernetes operations -- Git +- Node registration & lease management +- Workload scheduling +- Resource-aware placement +- Desired-state reconciliation +- Retry & backoff policies +- etcd state persistence +- Event emission +- Health & metrics endpoints -### 1. Clone the Repository +## persys-gateway -```bash -git clone https://github.com/persys-dev/persys-cloud.git -cd persys-cloud -``` +Platform entrypoint. -### 2. Choose Your Setup Method +Responsibilities: -#### Option A: Docker Compose (Recommended for Development) +- REST APIs +- mTLS authentication +- Request routing to scheduler +- Webhook ingestion (future use) +- Platform-level authorization -```bash -make up -``` +## persys-cfssl -This initializes the complete platform in a local Docker environment using docker-compose. +Certificate authority bootstrap. -#### Option B: Kind (Local Kubernetes) +- Service identity issuance +- mTLS trust chain +- Agent certificate provisioning -```bash -make kind -``` +## persys-federation -This builds all Docker images locally, generates Kubernetes deployment files, and deploys to a Kind cluster. +Future hybrid/multi-cloud integration layer. -#### Option C: Shell Script +- Connect to AWS/GCP/other providers +- Offload workloads +- Aggregate compute resources -```bash -chmod +x init.sh -./init.sh -``` +## persys-agent (runtime node agent) -### 3. Access the Platform +Responsibilities: -Once deployed, you can interact with Persys Cloud using: +- Register with scheduler +- Maintain heartbeat +- Enforce resource limits +- Execute containers / compose / VMs +- Report workload status +- Perform local garbage collection +- Expose Prometheus metrics -- **REST API**: Direct HTTP requests to the API Gateway -- **CLI Client**: Command-line interface (when available) -- **Postman**: API testing and automation +Agent is intentionally simple and execution-focused. -### 4. Deploy Your First Workload +------------------------------------------------------------------------ -1. **Register Nodes**: Add Docker-based Linux nodes to the cluster -2. **Create Workload**: Define your container workload specification -3. **Schedule Workload**: Submit workload to the Prow Scheduler -4. **Monitor**: Track deployment status and container health -5. **Scale**: Automatically scale workloads based on demand +# Scheduling Model -## Local Development +1. Agent boots +2. Agent registers with scheduler via gRPC +3. Scheduler stores node in etcd and issues lease +4. Agent sends periodic heartbeat +5. Scheduler tracks node liveness +6. User submits workload +7. Scheduler selects node based on: + - CPU availability + - Memory availability + - Disk pools + - Labels + - Capability matching +8. Scheduler sends ApplyWorkload +9. Agent executes and reports status +10. Reconciler enforces desired state -### Development Environment Setup +------------------------------------------------------------------------ -1. **Clone and Setup**: - ```bash - git clone https://github.com/persys-dev/persys-cloud.git - cd persys-cloud - ``` +# Supported Workload Types -2. **Start Services**: - ```bash - # Using Docker Compose - make up - - # Or using Kind - make kind - ``` +## Containers -3. **Access Services**: - - API Gateway: `http://localhost:8551` - - Prow Scheduler: `http://localhost:8084` - - Cloud Management: `http://localhost:9090` - - ETCD: `http://localhost:2379` - - CoreDNS: `http://localhost:53` +- Image-based +- Resource limits +- Env variables +- Volumes +- Ports +- Restart policies +- Privileged mode (optional) -### Development Workflow +## Docker Compose -1. Make changes to the codebase -2. Run tests: `make test` -3. Build images: `make build` -4. Deploy changes: `make deploy` -5. Verify functionality +- Git-based deployments +- Inline YAML support +- Environment injection +- Secret injection (future: Vault integration) +- Mixed public/private images -### Agent Development +## Virtual Machines -To develop or test the Persys Agent: +- vCPU & memory specification +- Disk provisioning via storage pools +- Cloud-init injection +- Network configuration +- Login credential provisioning +- Future: IP reporting back to scheduler -```bash -# Build the agent -cd prow -make build +------------------------------------------------------------------------ -# Run the agent locally -make run +# Cluster State Model (etcd) -# Test agent communication -curl -X POST http://localhost:8084/api/v1/nodes/register \ - -H "Content-Type: application/json" \ - -d '{"nodeId":"test-node","ipAddress":"localhost","agentPort":8080}' -``` +Scheduler persists: + +- /nodes/``{=html} +- /nodes/``{=html}/lease +- /workloads/``{=html} +- /assignments/``{=html} +- /events/``{=html} +- /retries/``{=html} + +This enables: + +- Auditability +- Recovery after scheduler restart +- Drift detection +- Retry tracking +- Failure history + +------------------------------------------------------------------------ -## Services +# Resource Enforcement -Persys Cloud consists of several microservices, each with its own README and documentation: +Agent enforces: -### Core Services +- CPU limits +- Memory limits +- Disk allocation +- System threshold rejection (default 80% utilization) +- Orphan resource cleanup +- Zombie workload detection -- **[API Gateway](api-gateway/)**: Main entry point for all API requests with authentication -- **[Prow Scheduler](prow/)**: Intelligent workload orchestration and scheduling -- **[Cloud Management](cloud-mgmt/)**: Multi-cloud infrastructure management -- **[Persys CFSSL](persys-cfssl/)**: Certificate management and PKI for mTLS -- **[Persys Operator](persys-operator/)**: Place Holder -- **[Persys Agent](persys-agent/)**: Place Holder -- **[Persys CLI](persys-cli/)**: Place Holder +Workloads are rejected early if capacity is insufficient. -### Infrastructure +------------------------------------------------------------------------ -- **[Infrastructure](infra/)**: Terraform configurations, Kubernetes manifests, and deployment scripts -- **[Documentation](docs/)**: Comprehensive documentation and guides +# Reliability Model -Each service directory contains detailed README files explaining the component's purpose, architecture, and ownership. +Persys Compute uses: -## Documentation +- Lease-based node liveness +- Heartbeat TTL enforcement +- Idempotent workload operations +- Explicit failure reasons (enum-based) +- Garbage collection loops +- Structured error propagation +- Metrics-first observability -Comprehensive documentation is available in the [docs/](docs/) directory: +No ghost workloads. No silent failures. No hidden retries. -- **[Getting Started](docs/getting-started.md)**: Step-by-step guide for new users -- **[How It Works](docs/how-it-works.md)**: Detailed explanation of system components -- **[Installation](docs/install.md)**: Installation and deployment instructions -- **[Architecture](docs/architecture.md)**: System architecture and design decisions -- **[Contributing](docs/contributions.md)**: Guidelines for contributors +------------------------------------------------------------------------ -## Contributing +# Observability -We welcome contributions from the community! Here's how you can help: +Each component exposes: -### Ways to Contribute +- /metrics (Prometheus) +- /health +- Structured logs -- 🐛 **Report Bugs**: Open an issue with detailed bug reports -- 💡 **Feature Requests**: Suggest new features and improvements -- 🔧 **Code Contributions**: Submit pull requests with code changes -- 📚 **Documentation**: Help improve our documentation -- 🧪 **Testing**: Test features and report issues +Metrics include: -### Getting Started +- Workload counts +- Failure reasons +- Apply duration +- Resource utilization +- Runtime health status +- GC statistics -1. Fork the repository -2. Create a feature branch: `git checkout -b feature/amazing-feature` -3. Make your changes -4. Add tests for new functionality -5. Commit your changes: `git commit -m 'Add amazing feature'` -6. Push to the branch: `git push origin feature/amazing-feature` -7. Open a Pull Request +------------------------------------------------------------------------ -### Development Guidelines +# Local Development -- Follow Go coding standards and best practices -- Add tests for new functionality -- Update documentation for any changes -- Ensure all tests pass before submitting PRs -- Use conventional commit messages +Prerequisites: -## Community +- Go 1.24+ +- Docker -Join our growing community of developers and contributors: +Start etcd: + +``` bash +./hack/start-etcd-docker.sh +``` + +Run scheduler (dev mode): + +``` bash +cd persys-scheduler +go run ./cmd/scheduler -insecure +``` + +Build services: + +``` bash +cd persys-scheduler && go build ./cmd/scheduler +cd persys-gateway && go build ./cmd/main.go +``` -### Communication Channels +------------------------------------------------------------------------ -- **Slack**: [Join our Slack workspace](https://join.slack.com/t/persys-cicd/shared_invite/zt-1lje1wst0-E0TjKMIXGe1FGLex1uQoxg) -- **GitHub Issues**: [Report bugs and request features](https://github.com/persys-dev/persys-cloud/issues) -- **GitHub Discussions**: [Join discussions](https://github.com/persys-dev/persys-cloud/discussions) +# Project Philosophy -### Community Guidelines +Persys Compute is built around: -- Be respectful and inclusive -- Help others learn and grow -- Share knowledge and best practices -- Provide constructive feedback -- Follow the [Code of Conduct](CODE_OF_CONDUCT.md) +- Explicit contracts (protobuf-first design) +- Control-plane correctness +- Minimal but powerful abstraction +- Hyperscaler-inspired architecture +- Avoiding unnecessary AI buzz +- Strong separation of concerns +- Scalability from day one -## License +This is infrastructure designed to scale --- without rewriting the +system when adding more nodes. -This project is licensed under the MIT License - see the [LICENSE](LICENSE) file for details. +------------------------------------------------------------------------ -## Acknowledgments +# Roadmap Highlights -- Built with love by the Persys Cloud community -- Powered by open-source technologies including Docker, Kubernetes, ETCD, and CoreDNS -- Inspired by the Cloud Native Computing Foundation (CNCF) ecosystem +- Storage pool full implementation +- VM network introspection & IP reporting +- Retry engine with exponential backoff +- Federation workload offloading +- Secrets integration (Vault) +- Stream-based control channel +- Multi-cluster support ---- +------------------------------------------------------------------------ -**Persys Cloud** - Simplifying DevOps/SRE with intelligent workload orchestration. +# License -For more information, visit [https://github.com/persys-dev/persys-cloud](https://github.com/persys-dev/persys-cloud) +MIT diff --git a/api-gateway/CHANGELOG.md b/api-gateway/CHANGELOG.md deleted file mode 100755 index 14b7bf0..0000000 --- a/api-gateway/CHANGELOG.md +++ /dev/null @@ -1,8 +0,0 @@ -# Change Log - - -All notable changes to this project will be documented in this file. - -## [unreleased] - -* events manager is going to use etcd instead of mongoDB so we need to change api gateway accordingly \ No newline at end of file diff --git a/api-gateway/catalog-info.yaml b/api-gateway/catalog-info.yaml deleted file mode 100755 index 86b814d..0000000 --- a/api-gateway/catalog-info.yaml +++ /dev/null @@ -1,22 +0,0 @@ -apiVersion: backstage.io/v1alpha1 -kind: Component -metadata: - name: api-gateway - description: The place to be, for great artists - labels: - example.com/custom: custom_label_value - annotations: - example.com/service-discovery: artistweb - circleci.com/project-slug: github/example-org/artist-website - tags: - - golang - links: - - url: https://admin.example-org.com - title: Admin Dashboard - icon: dashboard - type: admin-dashboard -spec: - type: website - lifecycle: production - owner: artist-relations-team - system: public-websites \ No newline at end of file diff --git a/api-gateway/config.toml b/api-gateway/config.toml deleted file mode 100644 index fcb0803..0000000 --- a/api-gateway/config.toml +++ /dev/null @@ -1,47 +0,0 @@ -[app] -httpAddr = ":8551" -grpcAddr = ":8661" -httpAddrNonMTLS = "8085" -storage = "mongodb" - -[app.metadata] -name = "api_gateway" - - -[database] -mongoURI= "mongodb://admin:admin@mongodb:27017/api-gateway?authSource=admin" -collections = ["events", "repos", "users"] -name= "api-gateway" - -[tls] -certPath="/etc/api-gateway/certs/tls.crt" -keyPath="/etc/api-gateway/certs/tls.key" -caPath="/etc/api-gateway/certs/ca.pem" - -[coreDNS] -addr= "coredns:53" - -[cfssl] -apiUrl= "https://persys-cfssl:8888" - -# Prow scheduler configuration for proxying -[prow] -schedulerAddr = "prow-scheduler:8085" -enableProxy = true -discoveryDomain = "persys.local" -discoveryService = "_prow-scheduler" - -[github] -webHookURL = "http://persys.eastus.cloudapp.azure.com/webhook" -webHookSecret = "SECRET" - -[github.auth] -clientID = "SECRET" -clientSecret = "SECRET" - -[log] -lokiEndpoint="http://loki:3301" -level = "debug" - -[telemetry] -addr = "http://jaeger:14268/api/traces" \ No newline at end of file diff --git a/cloud-mgmt/cmd/main.go b/cloud-mgmt/cmd/main.go deleted file mode 100755 index a0ff874..0000000 --- a/cloud-mgmt/cmd/main.go +++ /dev/null @@ -1,134 +0,0 @@ -package main - -import ( - "context" - "log" - "net" - - "github.com/persys-dev/persys-cloud/cloud-mgmt/config" - "github.com/persys-dev/persys-cloud/cloud-mgmt/gapi" - "github.com/persys-dev/persys-cloud/cloud-mgmt/internal/providers" - "github.com/persys-dev/persys-cloud/cloud-mgmt/proto" - "github.com/persys-dev/persys-cloud/cloud-mgmt/services" - "github.com/persys-dev/persys-cloud/cloud-mgmt/utils" - "github.com/redis/go-redis/v9" - "go.mongodb.org/mongo-driver/mongo" - "go.mongodb.org/mongo-driver/mongo/options" - "go.mongodb.org/mongo-driver/mongo/readpref" - "go.opentelemetry.io/otel" - "go.opentelemetry.io/otel/exporters/jaeger" - "go.opentelemetry.io/otel/sdk/resource" - "go.opentelemetry.io/otel/sdk/trace" - semconv "go.opentelemetry.io/otel/semconv/v1.17.0" - "google.golang.org/grpc" - "google.golang.org/grpc/credentials" - "google.golang.org/grpc/reflection" -) - -var ( - ctx context.Context - mongoClient *mongo.Client - redisClient *redis.Client - cloudColl *mongo.Collection - cloudSvc services.CloudService -) - -func init() { - config, err := config.ReadConfig() - if err != nil { - utils.AuditLog("Failed to load config: %v", err) - log.Fatal(err) - } - - ctx = context.Background() - - // Initialize MongoDB - mongoConn := options.Client().ApplyURI(config.MongoURI). - SetRetryWrites(true). - SetRetryReads(true) - mongoClient, err = mongo.Connect(ctx, mongoConn) - if err != nil { - utils.AuditLog("MongoDB connection failed: %v", err) - log.Fatal(err) - } - if err := mongoClient.Ping(ctx, readpref.Primary()); err != nil { - utils.AuditLog("MongoDB ping failed: %v", err) - log.Fatal(err) - } - cloudColl = mongoClient.Database("cloud-mgmt").Collection("environments") - - // Initialize Redis - redisClient = redis.NewClient(&redis.Options{ - Addr: config.RedisAddr, - Password: config.RedisPassword, - }) - if _, err := redisClient.Ping(ctx).Result(); err != nil { - utils.AuditLog("Redis connection failed: %v", err) - log.Fatal(err) - } - - // Initialize Tracing - tp, err := initTracer(config.JaegerEndpoint) - if err != nil { - utils.AuditLog("Tracer initialization failed: %v", err) - log.Fatal(err) - } - otel.SetTracerProvider(tp) - - // Initialize Cloud Service - providerMgr := providers.NewManager(config, redisClient) - cloudSvc = services.NewCloudService(cloudColl, ctx, providerMgr) -} - -func initTracer(endpoint string) (*trace.TracerProvider, error) { - exporter, err := jaeger.New(jaeger.WithCollectorEndpoint(jaeger.WithEndpoint(endpoint))) - if err != nil { - return nil, err - } - tp := trace.NewTracerProvider( - trace.WithBatcher(exporter), - trace.WithResource(resource.NewWithAttributes( - semconv.SchemaURL, - semconv.ServiceNameKey.String("cloud-mgmt"), - )), - ) - return tp, nil -} - -func startGrpcServer(config *config.Config) { - creds, err := credentials.NewServerTLSFromFile(config.TLSCert, config.TLSKey) - if err != nil { - utils.AuditLog("Failed to load TLS credentials: %v", err) - log.Fatal(err) - } - - grpcServer := grpc.NewServer(grpc.Creds(creds)) - cloudServer, err := gapi.NewGrpcCloudServer(config, cloudSvc) - if err != nil { - utils.AuditLog("Failed to create gRPC server: %v", err) - log.Fatal(err) - } - - pb.RegisterCloudMgmtServiceServer(grpcServer, cloudServer) - reflection.Register(grpcServer) - - listener, err := net.Listen("tcp", config.GrpcServerAddress) - if err != nil { - utils.AuditLog("Failed to start gRPC listener: %v", err) - log.Fatal(err) - } - - log.Printf("gRPC server started on %s", config.GrpcServerAddress) - if err := grpcServer.Serve(listener); err != nil { - utils.AuditLog("gRPC server failed: %v", err) - log.Fatal(err) - } -} - -func main() { - config, err := config.ReadConfig() - if err != nil { - log.Fatal(err) - } - startGrpcServer(config) -} \ No newline at end of file diff --git a/cloud-mgmt/go.mod b/cloud-mgmt/go.mod deleted file mode 100755 index 60f4ffe..0000000 --- a/cloud-mgmt/go.mod +++ /dev/null @@ -1,113 +0,0 @@ -module github.com/persys-dev/persys-cloud/cloud-mgmt - -go 1.24 - -require ( - github.com/ThreeDotsLabs/watermill v1.1.1 - github.com/ThreeDotsLabs/watermill-kafka/v2 v2.2.2 - github.com/spf13/viper v1.13.0 - go.mongodb.org/mongo-driver v1.11.1 - google.golang.org/grpc v1.52.0 - google.golang.org/protobuf v1.28.1 - k8s.io/apimachinery v0.27.2 - k8s.io/client-go v0.27.2 - sigs.k8s.io/cluster-api v1.3.3 - sigs.k8s.io/cluster-api-provider-vsphere v1.6.1 -) - -require ( - github.com/Shopify/sarama v1.32.0 // indirect - github.com/beorn7/perks v1.0.1 // indirect - github.com/blang/semver v3.5.1+incompatible // indirect - github.com/cenkalti/backoff/v3 v3.0.0 // indirect - github.com/cespare/xxhash/v2 v2.1.2 // indirect - github.com/davecgh/go-spew v1.1.1 // indirect - github.com/eapache/go-resiliency v1.2.0 // indirect - github.com/eapache/go-xerial-snappy v0.0.0-20180814174437-776d5712da21 // indirect - github.com/eapache/queue v1.1.0 // indirect - github.com/emicklei/go-restful/v3 v3.9.0 // indirect - github.com/evanphx/json-patch/v5 v5.6.0 // indirect - github.com/fsnotify/fsnotify v1.6.0 // indirect - github.com/go-logr/logr v1.2.3 // indirect - github.com/go-logr/stdr v1.2.2 // indirect - github.com/go-openapi/jsonpointer v0.19.6 // indirect - github.com/go-openapi/jsonreference v0.20.1 // indirect - github.com/go-openapi/swag v0.22.3 // indirect - github.com/gogo/protobuf v1.3.2 // indirect - github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da // indirect - github.com/golang/protobuf v1.5.3 // indirect - github.com/golang/snappy v0.0.4 // indirect - github.com/google/gnostic v0.6.9 // indirect - github.com/google/go-cmp v0.5.9 // indirect - github.com/google/gofuzz v1.2.0 // indirect - github.com/google/uuid v1.3.0 // indirect - github.com/hashicorp/errwrap v1.1.0 // indirect - github.com/hashicorp/go-multierror v1.1.1 // indirect - github.com/hashicorp/go-uuid v1.0.2 // indirect - github.com/hashicorp/hcl v1.0.0 // indirect - github.com/imdario/mergo v0.3.13 // indirect - github.com/jcmturner/aescts/v2 v2.0.0 // indirect - github.com/jcmturner/dnsutils/v2 v2.0.0 // indirect - github.com/jcmturner/gofork v1.0.0 // indirect - github.com/jcmturner/gokrb5/v8 v8.4.2 // indirect - github.com/jcmturner/rpc/v2 v2.0.3 // indirect - github.com/josharian/intern v1.0.0 // indirect - github.com/json-iterator/go v1.1.12 // indirect - github.com/klauspost/compress v1.15.1 // indirect - github.com/lithammer/shortuuid/v3 v3.0.7 // indirect - github.com/magiconair/properties v1.8.6 // indirect - github.com/mailru/easyjson v0.7.7 // indirect - github.com/matttproud/golang_protobuf_extensions v1.0.2-0.20181231171920-c182affec369 // indirect - github.com/mitchellh/mapstructure v1.5.0 // indirect - github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect - github.com/modern-go/reflect2 v1.0.2 // indirect - github.com/montanaflynn/stats v0.0.0-20171201202039-1bf9dbcd8cbe // indirect - github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect - github.com/oklog/ulid v1.3.1 // indirect - github.com/pelletier/go-toml v1.9.5 // indirect - github.com/pelletier/go-toml/v2 v2.0.5 // indirect - github.com/pierrec/lz4 v2.6.1+incompatible // indirect - github.com/pkg/errors v0.9.1 // indirect - github.com/prometheus/client_golang v1.13.0 // indirect - github.com/prometheus/client_model v0.2.0 // indirect - github.com/prometheus/common v0.37.0 // indirect - github.com/prometheus/procfs v0.8.0 // indirect - github.com/rcrowley/go-metrics v0.0.0-20201227073835-cf1acfcdf475 // indirect - github.com/spf13/afero v1.9.2 // indirect - github.com/spf13/cast v1.5.0 // indirect - github.com/spf13/jwalterweatherman v1.1.0 // indirect - github.com/spf13/pflag v1.0.5 // indirect - github.com/subosito/gotenv v1.4.1 // indirect - github.com/xdg-go/pbkdf2 v1.0.0 // indirect - github.com/xdg-go/scram v1.1.1 // indirect - github.com/xdg-go/stringprep v1.0.3 // indirect - github.com/youmark/pkcs8 v0.0.0-20181117223130-1be2e3e5546d // indirect - go.opentelemetry.io/contrib/instrumentation/github.com/Shopify/sarama/otelsarama v0.31.0 // indirect - go.opentelemetry.io/otel v1.6.1 // indirect - go.opentelemetry.io/otel/trace v1.6.1 // indirect - golang.org/x/crypto v0.6.0 // indirect - golang.org/x/net v0.8.0 // indirect - golang.org/x/oauth2 v0.0.0-20221014153046-6fdb5e3db783 // indirect - golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4 // indirect - golang.org/x/sys v0.6.0 // indirect - golang.org/x/term v0.6.0 // indirect - golang.org/x/text v0.8.0 // indirect - golang.org/x/time v0.0.0-20220722155302-e5dcc9cfc0b9 // indirect - gomodules.xyz/jsonpatch/v2 v2.2.0 // indirect - google.golang.org/appengine v1.6.7 // indirect - google.golang.org/genproto v0.0.0-20221118155620-16455021b5e6 // indirect - gopkg.in/inf.v0 v0.9.1 // indirect - gopkg.in/ini.v1 v1.67.0 // indirect - gopkg.in/yaml.v2 v2.4.0 // indirect - gopkg.in/yaml.v3 v3.0.1 // indirect - k8s.io/api v0.27.2 // indirect - k8s.io/apiextensions-apiserver v0.25.0 // indirect - k8s.io/component-base v0.25.0 // indirect - k8s.io/klog/v2 v2.100.1 // indirect - k8s.io/kube-openapi v0.0.0-20230501164219-8b0f38b5fd1f // indirect - k8s.io/utils v0.0.0-20230209194617-a36077c30491 // indirect - sigs.k8s.io/controller-runtime v0.13.1 // indirect - sigs.k8s.io/json v0.0.0-20221116044647-bc3834ca7abd // indirect - sigs.k8s.io/structured-merge-diff/v4 v4.2.3 // indirect - sigs.k8s.io/yaml v1.3.0 // indirect -) diff --git a/cloud-mgmt/internal/providers/persys/persys.go b/cloud-mgmt/internal/providers/persys/persys.go deleted file mode 100644 index b3ba50f..0000000 --- a/cloud-mgmt/internal/providers/persys/persys.go +++ /dev/null @@ -1,145 +0,0 @@ -package providers - -import ( - "context" - "fmt" - "github.com/persys-dev/persys-cloud/cloud-mgmt/utils" - "k8s.io/apimachinery/pkg/api/errors" - metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" - "k8s.io/apimachinery/pkg/runtime" - "k8s.io/client-go/kubernetes" - "k8s.io/client-go/kubernetes/scheme" - "k8s.io/client-go/rest" - "k8s.io/client-go/tools/clientcmd" - "log" - capvv1beta1 "sigs.k8s.io/cluster-api-provider-vsphere/apis/v1beta1" - capi "sigs.k8s.io/cluster-api/api/v1beta1" -) - -var ( - kubeConfig *rest.Config -) - -func getKubeConfig() *kubernetes.Clientset { - var err error - - err = utils.DownloadFile("persys", "kube-config.yaml") - - if err != nil { - log.Fatalf("kube config didnt download") - } - - config, err := clientcmd.BuildConfigFromFlags("", "/kube-config.yaml") - - config, err = rest.InClusterConfig() - - if err != nil { - log.Fatalf("Error creating Kubernetes config: %v\n", err) - } - - clientSet, err := kubernetes.NewForConfig(config) - - if err != nil { - log.Fatalf("Error creating Kubernetes client: %v\n", err) - } - - return clientSet -} - -func CreateCluster() error { - clientset := getKubeConfig() - - // Register the CAPV types with the Kubernetes scheme - if err := capvv1beta1.AddToScheme(scheme.Scheme); err != nil { - fmt.Printf("Error adding CAPV to scheme: %v\n", err) - return err - } - // Create a new CAPV VSphereCluster object - vsphereCluster := &capvv1beta1.VSphereCluster{ - TypeMeta: metav1.TypeMeta{ - APIVersion: capvv1beta1.GroupVersion.String(), - Kind: "VSphereCluster", - }, - ObjectMeta: metav1.ObjectMeta{ - Name: "my-cluster", - Namespace: "default", - }, - Spec: capvv1beta1.VSphereClusterSpec{}, - } - // Create the CAPI kubernetes cluster object - cluster := &capi.Cluster{ - TypeMeta: metav1.TypeMeta{ - APIVersion: capi.GroupVersion.String(), - Kind: "Cluster", - }, - ObjectMeta: metav1.ObjectMeta{ - Name: "my-cluster", - Namespace: "default", - }, - Spec: capi.ClusterSpec{}, - } - // Create the cluster using the Kubernetes API - err := createOrUpdateCluster(context.Background(), clientset, vsphereCluster, cluster) - if err != nil { - fmt.Printf("Error creating or updating cluster: %v\n", err) - return err - } - - fmt.Printf("Cluster created or updated: %s\n", cluster.Name) - - return nil -} - -func createOrUpdateCluster(ctx context.Context, clientset *kubernetes.Clientset, vsphereCluster *capvv1beta1.VSphereCluster, cluster *capi.Cluster) error { - client := clientset.RESTClient() - // Encode the VSphereCluster and Cluster objects using the runtime encoder - vsphereClusterData, err := runtime.Encode(scheme.Codecs.LegacyCodec(capvv1beta1.GroupVersion), vsphereCluster) - if err != nil { - return fmt.Errorf("failed to encode VSphereCluster: %v", err) - } - clusterData, err := runtime.Encode(scheme.Codecs.LegacyCodec(capi.GroupVersion), cluster) - if err != nil { - return fmt.Errorf("failed to encode Cluster: %v", err) - } - // Send a POST request to create the VSphereCluster object - err = client.Post(). - Namespace(vsphereCluster.Namespace). - Resource("vsphereclusters"). - Body(vsphereClusterData). - Do(ctx). - Error() - if errors.IsAlreadyExists(err) { - // If the VSphereCluster object already exists, send a PUT request to update it - err = client.Put(). - Namespace(vsphereCluster.Namespace). - Resource("vsphereclusters"). - Name(vsphereCluster.Name). - Body(vsphereClusterData). - Do(ctx). - Error() - } - if err != nil { - return fmt.Errorf("failed to create or update VSphereCluster: %v", err) - } - // Send a POST request to create the Cluster object - err = client.Post(). - Namespace(cluster.Namespace). - Resource("clusters"). - Body(clusterData). - Do(ctx). - Error() - if errors.IsAlreadyExists(err) { - // If the Cluster object already exists, send a PUT request to update it - err = client.Put(). - Namespace(cluster.Namespace). - Resource("clusters"). - Name(cluster.Name). - Body(clusterData). - Do(ctx). - Error() - } - if err != nil { - return fmt.Errorf("failed to create or update Cluster: %v", err) - } - return nil -} diff --git a/cloud-mgmt/internal/providers/provider.go b/cloud-mgmt/internal/providers/provider.go deleted file mode 100644 index b88628d..0000000 --- a/cloud-mgmt/internal/providers/provider.go +++ /dev/null @@ -1,108 +0,0 @@ -package providers - -import ( - "context" - "fmt" - "github.com/persys-dev/persys-cloud/cloud-mgmt/config" - "github.com/redis/go-redis/v9" - "github.com/hashicorp/terraform-exec/tfexec" - capav1beta1 "sigs.k8s.io/cluster-api-provider-aws/v2/api/v1beta1" - capzv1beta1 "sigs.k8s.io/cluster-api-provider-azure/v2/api/v1beta1" - capgv1beta1 "sigs.k8s.io/cluster-api-provider-gcp/v2/api/v1beta1" - capvv1beta1 "sigs.k8s.io/cluster-api-provider-vsphere/apis/v1beta1" - capi "sigs.k8s.io/cluster-api/api/v1beta1" -) - -type Provider interface { - Provision(ctx context.Context, envID string, spec *EnvironmentSpec) error - Update(ctx context.Context, envID string, spec *EnvironmentSpec) error - Delete(ctx context.Context, envID string) error -} - -type Manager struct { - config *config.Config - redis *redis.Client - aws Provider - azure Provider - gcp Provider - vsphere Provider - onprem Provider -} - -type EnvironmentSpec struct { - Provider string `json:"provider"` - ClusterName string `json:"cluster_name"` - Namespace string `json:"namespace"` - K8sVersion string `json:"k8s_version"` - NodeCount int `json:"node_count"` - Region string `json:"region"` - Credentials map[string]string `json:"credentials"` - InfraConfig map[string]string `json:"infra_config"` -} - -func NewManager(config *config.Config, redis *redis.Client) *Manager { - return &Manager{ - config: config, - redis: redis, - aws: &AWSProvider{config: config}, - azure: &AzureProvider{config: config}, - gcp: &GCPProvider{config: config}, - vsphere: &VSphereProvider{config: config}, - onprem: &OnPremProvider{config: config}, - } -} - -func (m *Manager) GetProvider(provider string) (Provider, error) { - switch provider { - case "aws": - return m.aws, nil - case "azure": - return m.azure, nil - case "gcp": - return m.gcp, nil - case "vsphere": - return m.vsphere, nil - case "onprem": - return m.onprem, nil - default: - return nil, fmt.Errorf("unsupported provider: %s", provider) - } -} - -type AWSProvider struct { - config *config.Config -} - -func (p *AWSProvider) Provision(ctx context.Context, envID string, spec *EnvironmentSpec) error { - // Initialize Terraform - tf, err := tfexec.NewTerraform("/path/to/terraform", "/path/to/terraform") - if err != nil { - return err - } - // Apply Terraform for VPC, subnets, etc. - // Example: tf.Apply(ctx, tfexec.Dir("/path/to/aws_infra")) - - // Create CAPI AWSCluster - cluster := &capav1beta1.AWSCluster{ - // Populate spec with spec.Region, spec.Credentials, etc. - } - // Apply CAPI resources - return applyCAPICluster(ctx, cluster, spec) -} - -type AzureProvider struct { - config *config.Config -} - -func (p *AzureProvider) Provision(ctx context.Context, envID string, spec *EnvironmentSpec) error { - // Similar logic for Azure using CAPZ - return nil -} - -// Implement GCPProvider, VSphereProvider, OnPremProvider similarly - -func applyCAPICluster(ctx context.Context, cluster interface{}, spec *EnvironmentSpec) error { - // Use Kubernetes client to apply CAPI resources - // Similar to persys.CreateCluster but generalized - return nil -} \ No newline at end of file diff --git a/cloud-mgmt/tests/suite_test.go b/cloud-mgmt/tests/suite_test.go deleted file mode 100644 index 8549f2d..0000000 --- a/cloud-mgmt/tests/suite_test.go +++ /dev/null @@ -1,64 +0,0 @@ -package tests - -import ( - "context" - "github.com/persys-dev/persys-cloud/cloud-mgmt/config" - "github.com/persys-dev/persys-cloud/cloud-mgmt/internal/cloud-provider/persys" - pb "github.com/persys-dev/persys-cloud/cloud-mgmt/proto" - "github.com/persys-dev/persys-cloud/cloud-mgmt/services" - "github.com/stretchr/testify/suite" - "go.mongodb.org/mongo-driver/mongo" - "go.mongodb.org/mongo-driver/mongo/options" - "go.mongodb.org/mongo-driver/mongo/readpref" - "google.golang.org/grpc" - "testing" -) - -type TestSuite struct { - suite.Suite - cloudService services.CloudService - conn *grpc.ClientConn - mongoClient *mongo.Client -} - -// SetupTest initializes the test suite -func (suite *TestSuite) SetupTest() { - config, err := config.ReadConfig() - suite.Require().NoError(err) - options := options.Client().ApplyURI(config.MongoURI) - suite.mongoClient, err = mongo.Connect(context.Background(), options) - suite.Require().NoError(err) - err = suite.mongoClient.Ping(context.Background(), readpref.Primary()) - suite.Require().NoError(err) - err = persys.CreateCluster() - suite.Require().NoError(err) - suite.cloudService = services.NewCloudService(suite.mongoClient.Database("cloud-mgmt"), context.Background()) - suite.conn, err = grpc.Dial("config.GrpcServerAddress", grpc.WithInsecure()) - suite.Require().NoError(err) -} - -// TearDownTest cleans up after each test -func (suite *TestSuite) TearDownTest() { - err := suite.conn.Close() - suite.Require().NoError(err) - err = suite.mongoClient.Disconnect(context.Background()) - suite.Require().NoError(err) -} - -// TestCloudAction tests the CloudAction function -func (suite *TestSuite) TestCloudAction() { - client := pb.NewCloudMgmtServiceClient(suite.conn) - request := &pb.CloudRequest{Name: "test"} - response, err := client.CloudAction(context.Background(), request) - suite.Require().NoError(err) - suite.Require().NotNil(response) - suite.Equal(persys.CloudActionSuccess, response.Status) - request = &pb.CloudRequest{Name: ""} - response, err = client.CloudAction(context.Background(), request) - suite.Require().NoError(err) - suite.Require().NotNil(response) - suite.Equal(persys.CloudActionFailure, response.Status) -} -func TestSuiteRunner(t *testing.T) { - suite.Run(t, new(TestSuite)) -} diff --git a/compute-agent b/compute-agent new file mode 160000 index 0000000..5b967af --- /dev/null +++ b/compute-agent @@ -0,0 +1 @@ +Subproject commit 5b967af001a7c8bbc9431276902bcbd56b30ce24 diff --git a/docs/PROW_FORGERY_COMMUNICATION.md b/docs/PROW_FORGERY_COMMUNICATION.md new file mode 100644 index 0000000..764b022 --- /dev/null +++ b/docs/PROW_FORGERY_COMMUNICATION.md @@ -0,0 +1,161 @@ +# Prow-Forgery Communication via Redis + +## Architecture Overview + +``` +GitHub Webhook → API Gateway → Prow → Redis → Forgery +``` + +1. **API Gateway** receives GitHub webhooks and forwards them to Prow +2. **Prow** processes webhooks and queues build requests to Forgery via Redis +3. **Forgery** consumes build requests from Redis and executes builds + +## JSON Contract + +### Build Queue Message (Redis) + +The message sent from Prow to Forgery via Redis follows this structure: + +```json +{ + "id": "unique-build-id", + "project_name": "org/repo", + "type": "dockerfile|compose|pipeline", + "source": "https://github.com/org/repo.git", + "commit_hash": "abc123def456", + "branch": "main", + "strategy": "local|operator|prow", + "push_artifact": true, + "webhook_data": { + "event_type": "push|pull_request|tag", + "repository": "org/repo", + "sender": "username", + "ref": "refs/heads/main", + "before": "old-commit", + "after": "new-commit", + "pull_request": { + "number": 123, + "title": "Feature PR", + "state": "open" + } + }, + "metadata": { + "triggered_by": "webhook", + "correlation_id": "correlation-uuid", + "processed_by": "prow" + }, + "created_at": "2024-01-01T00:00:00Z" +} +``` + +### Field Descriptions + +| Field | Type | Required | Description | +|-------|------|----------|-------------| +| `id` | string | Yes | Unique build identifier | +| `project_name` | string | Yes | Repository name (org/repo format) | +| `type` | string | Yes | Build type: dockerfile, compose, pipeline | +| `source` | string | Yes | Git repository URL | +| `commit_hash` | string | Yes | Git commit hash to build | +| `branch` | string | No | Git branch name | +| `strategy` | string | Yes | Build strategy: local, operator, prow | +| `push_artifact` | boolean | No | Whether to push built artifacts | +| `webhook_data` | object | No | Original webhook information | +| `metadata` | object | No | Additional metadata for tracing | +| `created_at` | timestamp | No | Message creation timestamp | + +## Implementation Components + +### 1. Prow Build Service (`prow/internal/services/build_service.go`) + +- Processes webhook payloads from API Gateway +- Creates build queue messages +- Sends messages to Redis queue `forge:builds` + +### 2. Prow Webhook Handler (`prow/internal/api/webhook.go`) + +- Receives webhooks from API Gateway +- Uses BuildService to process and queue builds +- Returns success/error responses + +### 3. Forgery Redis Worker (`persys-forgery/internal/queue/worker.go`) + +- Consumes messages from Redis queue `forge:builds` +- Unmarshals into `models.BuildRequest` +- Executes builds using the orchestrator + +### 4. Enhanced BuildRequest Model (`persys-forgery/internal/models/build.go`) + +- Extended to include webhook data and metadata +- Compatible with the Redis message format +- Supports traceability and correlation + +## Redis Configuration + +### Queue Details +- **Queue Name**: `forge:builds` +- **Direction**: Prow → Forgery (LPush → BLPop) +- **Message Format**: JSON string +- **Priority**: FIFO (First In, First Out) + +### Redis Connection +```go +rdb := redis.NewClient(&redis.Options{ + Addr: "localhost:6379", + Password: "", + DB: 0, +}) +``` + +## Message Flow + +1. **GitHub Event**: Push/Pull Request to repository +2. **API Gateway**: Receives webhook, forwards to Prow +3. **Prow**: + - Receives webhook at `/webhook/github` + - Processes with `BuildService.ProcessWebhook()` + - Creates `BuildQueueMessage` + - Pushes to Redis: `LPush("forge:builds", messageJSON)` +4. **Forgery**: + - Worker consumes: `BLPop("forge:builds")` + - Unmarshals to `BuildRequest` + - Executes build with `orchestrator.BuildWithStrategy()` + +## Error Handling + +- **Redis Connection**: Retry logic in worker +- **Message Parsing**: Log errors, continue processing +- **Build Failures**: Handled by Forgery orchestrator +- **Webhook Validation**: Required field checks in Prow + +## Testing + +Use the test script `test-redis-communication.go` to verify: +- Message serialization/deserialization +- Redis queue operations +- JSON contract compatibility + +## Configuration + +### Prow Configuration +```yaml +redis: + addr: "localhost:6379" + password: "" + db: 0 +``` + +### Forgery Configuration +```yaml +redis: + addr: "localhost:6379" + password: "" + db: 0 +``` + +## Security Considerations + +- Webhook signature verification in API Gateway +- Redis authentication (if needed) +- Input validation in all components +- Correlation IDs for request tracing \ No newline at end of file diff --git a/infra/docker/docker-compose.yml b/infra/docker/docker-compose.yml index db18981..e1262c7 100755 --- a/infra/docker/docker-compose.yml +++ b/infra/docker/docker-compose.yml @@ -7,11 +7,11 @@ services: - # --- API Gateway (Entry point for all API requests) --- - api-gateway: - build: ../../api-gateway + # --- Persys Gateway (Entry point for all API requests) --- + persys-gateway: + build: ../../persys-gateway volumes: - - certs:/etc/api-gateway/certs + - certs:/etc/persys-gateway/certs ports: - 8551:8551 depends_on: @@ -19,15 +19,15 @@ services: - coredns - jaeger environment: - - OTEL_EXPORTER_JAEGER_ENDPOINT=jaegar:4318 + - OTEL_EXPORTER_JAEGER_ENDPOINT=jaeger:4318 networks: - - milx-cloud-net + - persys-cloud-net - # --- Prow Scheduler (Workload scheduler and reconciler) --- - persys-prow: - build: ../../prow + # --- Persys Scheduler (Workload scheduler and reconciler) --- + persys-scheduler: + build: ../../persys-scheduler volumes: - - certs:/etc/prow/certs + - certs:/etc/persys-scheduler/certs ports: - 8084:8084 # Non-mTLS API - 8085:8085 # mTLS API @@ -38,14 +38,14 @@ services: - jaeger environment: - ETCD_ENDPOINTS=etcd:2379 - - OTEL_EXPORTER_JAEGER_ENDPOINT=jaegar:4318 - - OTEL_SERVICE_NAME=prow-scheduler - - CA_FILE=/etc/prow/certs/ca.pem + - OTEL_EXPORTER_JAEGER_ENDPOINT=jaeger:4318 + - OTEL_SERVICE_NAME=persys-scheduler + - CA_FILE=/etc/persys-scheduler/certs/ca.pem - CFSSL_API_URL=https://persys-cfssl:8888 - - CERT_COMMON_NAME=prow-scheduler + - CERT_COMMON_NAME=persys-scheduler - CERT_ORGANIZATION=persys networks: - - milx-cloud-net + - persys-cloud-net # --- Certificate Authority (CFSSL) --- persys-cfssl: @@ -55,7 +55,7 @@ services: ports: - 8888:8888 networks: - - milx-cloud-net + - persys-cloud-net # --- CoreDNS (Service discovery) --- coredns: @@ -86,7 +86,7 @@ services: ports: - "9090:9090" networks: - - milx-cloud-net + - persys-cloud-net # --- Grafana (Metrics visualization) --- grafana: @@ -102,7 +102,7 @@ services: ports: - "3000:3000" networks: - - milx-cloud-net + - persys-cloud-net depends_on: - prometheus @@ -123,7 +123,7 @@ services: - "4317:4317" - "4318:4318" networks: - - milx-cloud-net + - persys-cloud-net # --- Node Exporter (Host metrics for Prometheus) --- node_exporter: @@ -133,7 +133,7 @@ services: ports: - '9100:9100' networks: - - milx-cloud-net + - persys-cloud-net # --- MongoDB (Database for API Gateway) --- mongodb: @@ -142,13 +142,13 @@ services: environment: MONGO_INITDB_ROOT_USERNAME: admin MONGO_INITDB_ROOT_PASSWORD: admin - MONGODB_DATABASE: api-gateway + MONGODB_DATABASE: persys-gateway ports: - 27017:27017 volumes: - mongodb_data_container:/data/db networks: - - milx-cloud-net + - persys-cloud-net # --- etcd (Key-value store for Prow and CoreDNS) --- etcd: @@ -157,7 +157,7 @@ services: ports: - "2379:2379" networks: - - milx-cloud-net + - persys-cloud-net # --- Volumes for persistent data and shared certs --- volumes: diff --git a/persys-agent b/persys-agent deleted file mode 160000 index 41b9e98..0000000 --- a/persys-agent +++ /dev/null @@ -1 +0,0 @@ -Subproject commit 41b9e986923f2b50a6a6926a8ebc065c07bc36c4 diff --git a/cloud-mgmt/Dockerfile b/persys-federation/Dockerfile similarity index 100% rename from cloud-mgmt/Dockerfile rename to persys-federation/Dockerfile diff --git a/cloud-mgmt/README.md b/persys-federation/README.md similarity index 100% rename from cloud-mgmt/README.md rename to persys-federation/README.md diff --git a/cloud-mgmt/cluster-config.yaml b/persys-federation/cluster-config.yaml similarity index 100% rename from cloud-mgmt/cluster-config.yaml rename to persys-federation/cluster-config.yaml diff --git a/persys-federation/cmd/main.go b/persys-federation/cmd/main.go new file mode 100755 index 0000000..7152511 --- /dev/null +++ b/persys-federation/cmd/main.go @@ -0,0 +1,146 @@ +package main + +import ( + "context" + "fmt" + "log" + "net" + "os" + "strings" + + "github.com/persys-dev/persys-cloud/persys-federation/config" + "github.com/persys-dev/persys-cloud/persys-federation/gapi" + "github.com/persys-dev/persys-cloud/persys-federation/internal/providers" + pb "github.com/persys-dev/persys-cloud/persys-federation/proto" + "github.com/persys-dev/persys-cloud/persys-federation/services" + "github.com/persys-dev/persys-cloud/persys-federation/utils" + "github.com/redis/go-redis/v9" + "go.mongodb.org/mongo-driver/mongo" + "go.mongodb.org/mongo-driver/mongo/options" + "go.mongodb.org/mongo-driver/mongo/readpref" + "go.opentelemetry.io/otel" + "go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp" + "go.opentelemetry.io/otel/sdk/resource" + "go.opentelemetry.io/otel/sdk/trace" + semconv "go.opentelemetry.io/otel/semconv/v1.17.0" + "google.golang.org/grpc" + "google.golang.org/grpc/reflection" +) + +var ( + ctx context.Context + mongoClient *mongo.Client + redisClient *redis.Client + cloudDB *mongo.Database + cloudSvc services.CloudService +) + +func init() { + config, err := config.ReadConfig() + if err != nil { + utils.AuditLog(fmt.Sprintf("Failed to load config: %v", err)) + log.Fatal(err) + } + + ctx = context.Background() + + // Initialize MongoDB + mongoConn := options.Client().ApplyURI(config.MongoURI). + SetRetryWrites(true). + SetRetryReads(true) + mongoClient, err = mongo.Connect(ctx, mongoConn) + if err != nil { + utils.AuditLog(fmt.Sprintf("MongoDB connection failed: %v", err)) + log.Fatal(err) + } + if err := mongoClient.Ping(ctx, readpref.Primary()); err != nil { + utils.AuditLog(fmt.Sprintf("MongoDB ping failed: %v", err)) + log.Fatal(err) + } + cloudDB = mongoClient.Database("persys-federation") + + // Initialize Redis + redisClient = redis.NewClient(&redis.Options{ + Addr: getenvDefault("REDIS_ADDR", "localhost:6379"), + Password: getenvDefault("REDIS_PASSWORD", ""), + }) + if _, err := redisClient.Ping(ctx).Result(); err != nil { + utils.AuditLog(fmt.Sprintf("Redis connection failed: %v", err)) + log.Fatal(err) + } + + // Initialize Tracing + tp, err := initTracer(getenvDefault("OTEL_EXPORTER_OTLP_ENDPOINT", "localhost:4318")) + if err != nil { + utils.AuditLog(fmt.Sprintf("Tracer initialization failed: %v", err)) + log.Fatal(err) + } + otel.SetTracerProvider(tp) + + // Initialize Cloud Service + providerMgr := providers.NewManager(config, redisClient) + cloudSvc = services.NewCloudService(cloudDB, ctx, providerMgr) +} + +func initTracer(endpoint string) (*trace.TracerProvider, error) { + opts := []otlptracehttp.Option{} + if strings.HasPrefix(endpoint, "http://") { + endpoint = strings.TrimPrefix(endpoint, "http://") + opts = append(opts, otlptracehttp.WithInsecure()) + } else if strings.HasPrefix(endpoint, "https://") { + endpoint = strings.TrimPrefix(endpoint, "https://") + } else { + opts = append(opts, otlptracehttp.WithInsecure()) + } + exporter, err := otlptracehttp.New(context.Background(), append(opts, otlptracehttp.WithEndpoint(endpoint))...) + if err != nil { + return nil, err + } + tp := trace.NewTracerProvider( + trace.WithBatcher(exporter), + trace.WithResource(resource.NewWithAttributes( + semconv.SchemaURL, + semconv.ServiceNameKey.String("cloud-mgmt"), + )), + ) + return tp, nil +} + +func startGrpcServer(config *config.Config) { + grpcServer := grpc.NewServer() + cloudServer, err := gapi.NewGrpcCloudServer(*config, cloudSvc) + if err != nil { + utils.AuditLog(fmt.Sprintf("Failed to create gRPC server: %v", err)) + log.Fatal(err) + } + + pb.RegisterCloudMgmtServiceServer(grpcServer, cloudServer) + reflection.Register(grpcServer) + + listener, err := net.Listen("tcp", config.GrpcAddr) + if err != nil { + utils.AuditLog(fmt.Sprintf("Failed to start gRPC listener: %v", err)) + log.Fatal(err) + } + + log.Printf("gRPC server started on %s", config.GrpcAddr) + if err := grpcServer.Serve(listener); err != nil { + utils.AuditLog(fmt.Sprintf("gRPC server failed: %v", err)) + log.Fatal(err) + } +} + +func getenvDefault(key, fallback string) string { + if v := strings.TrimSpace(os.Getenv(key)); v != "" { + return v + } + return fallback +} + +func main() { + config, err := config.ReadConfig() + if err != nil { + log.Fatal(err) + } + startGrpcServer(config) +} diff --git a/cloud-mgmt/config/config.go b/persys-federation/config/config.go similarity index 100% rename from cloud-mgmt/config/config.go rename to persys-federation/config/config.go diff --git a/cloud-mgmt/gapi/cloudmgmt-server.go b/persys-federation/gapi/cloudmgmt-server.go similarity index 65% rename from cloud-mgmt/gapi/cloudmgmt-server.go rename to persys-federation/gapi/cloudmgmt-server.go index dc9725c..31b08af 100644 --- a/cloud-mgmt/gapi/cloudmgmt-server.go +++ b/persys-federation/gapi/cloudmgmt-server.go @@ -1,9 +1,9 @@ package gapi import ( - "github.com/persys-dev/persys-cloud/cloud-mgmt/config" - pb "github.com/persys-dev/persys-cloud/cloud-mgmt/proto" - "github.com/persys-dev/persys-cloud/cloud-mgmt/services" + "github.com/persys-dev/persys-cloud/persys-federation/config" + pb "github.com/persys-dev/persys-cloud/persys-federation/proto" + "github.com/persys-dev/persys-cloud/persys-federation/services" ) type CloudServer struct { diff --git a/cloud-mgmt/gapi/rpc_kubeConfig.go b/persys-federation/gapi/rpc_kubeConfig.go similarity index 76% rename from cloud-mgmt/gapi/rpc_kubeConfig.go rename to persys-federation/gapi/rpc_kubeConfig.go index ada6d87..722e030 100644 --- a/cloud-mgmt/gapi/rpc_kubeConfig.go +++ b/persys-federation/gapi/rpc_kubeConfig.go @@ -2,7 +2,7 @@ package gapi import ( "context" - pb "github.com/persys-dev/persys-cloud/cloud-mgmt/proto" + pb "github.com/persys-dev/persys-cloud/persys-federation/proto" ) func (cloud *CloudServer) kubeConfig(ctx context.Context, req *pb.ServicesRequest) (*pb.ServicesResponse, error) { diff --git a/persys-federation/go.mod b/persys-federation/go.mod new file mode 100755 index 0000000..2132471 --- /dev/null +++ b/persys-federation/go.mod @@ -0,0 +1,67 @@ +module github.com/persys-dev/persys-cloud/persys-federation + +go 1.24.0 + +toolchain go1.24.13 + +require ( + github.com/hashicorp/terraform-exec v0.25.0 + github.com/redis/go-redis/v9 v9.18.0 + github.com/spf13/viper v1.13.0 + github.com/stretchr/testify v1.11.1 + go.mongodb.org/mongo-driver v1.11.1 + go.opentelemetry.io/otel v1.32.0 + go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.32.0 + go.opentelemetry.io/otel/sdk v1.32.0 + google.golang.org/grpc v1.67.1 + google.golang.org/protobuf v1.35.1 +) + +require ( + github.com/apparentlymart/go-textseg/v15 v15.0.0 // indirect + github.com/cenkalti/backoff/v4 v4.3.0 // indirect + github.com/cespare/xxhash/v2 v2.3.0 // indirect + github.com/davecgh/go-spew v1.1.1 // indirect + github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f // indirect + github.com/fsnotify/fsnotify v1.6.0 // indirect + github.com/go-logr/logr v1.4.3 // indirect + github.com/go-logr/stdr v1.2.2 // indirect + github.com/golang/snappy v0.0.4 // indirect + github.com/google/uuid v1.6.0 // indirect + github.com/grpc-ecosystem/grpc-gateway/v2 v2.23.0 // indirect + github.com/hashicorp/go-version v1.8.0 // indirect + github.com/hashicorp/hcl v1.0.0 // indirect + github.com/hashicorp/terraform-json v0.27.2 // indirect + github.com/klauspost/compress v1.15.1 // indirect + github.com/magiconair/properties v1.8.6 // indirect + github.com/mitchellh/mapstructure v1.5.0 // indirect + github.com/montanaflynn/stats v0.0.0-20171201202039-1bf9dbcd8cbe // indirect + github.com/pelletier/go-toml v1.9.5 // indirect + github.com/pelletier/go-toml/v2 v2.0.5 // indirect + github.com/pkg/errors v0.9.1 // indirect + github.com/pmezard/go-difflib v1.0.0 // indirect + github.com/spf13/afero v1.9.2 // indirect + github.com/spf13/cast v1.5.0 // indirect + github.com/spf13/jwalterweatherman v1.1.0 // indirect + github.com/spf13/pflag v1.0.5 // indirect + github.com/subosito/gotenv v1.4.1 // indirect + github.com/xdg-go/pbkdf2 v1.0.0 // indirect + github.com/xdg-go/scram v1.1.1 // indirect + github.com/xdg-go/stringprep v1.0.3 // indirect + github.com/youmark/pkcs8 v0.0.0-20181117223130-1be2e3e5546d // indirect + github.com/zclconf/go-cty v1.17.0 // indirect + go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.32.0 // indirect + go.opentelemetry.io/otel/metric v1.32.0 // indirect + go.opentelemetry.io/otel/trace v1.32.0 // indirect + go.opentelemetry.io/proto/otlp v1.3.1 // indirect + go.uber.org/atomic v1.11.0 // indirect + golang.org/x/crypto v0.45.0 // indirect + golang.org/x/net v0.47.0 // indirect + golang.org/x/sync v0.18.0 // indirect + golang.org/x/sys v0.38.0 // indirect + golang.org/x/text v0.31.0 // indirect + google.golang.org/genproto v0.0.0-20221118155620-16455021b5e6 // indirect + gopkg.in/ini.v1 v1.67.0 // indirect + gopkg.in/yaml.v2 v2.4.0 // indirect + gopkg.in/yaml.v3 v3.0.1 // indirect +) diff --git a/cloud-mgmt/go.sum b/persys-federation/go.sum similarity index 61% rename from cloud-mgmt/go.sum rename to persys-federation/go.sum index 87a37e4..322406f 100755 --- a/cloud-mgmt/go.sum +++ b/persys-federation/go.sum @@ -35,116 +35,74 @@ cloud.google.com/go/storage v1.6.0/go.mod h1:N7U0C8pVQ/+NIKOBQyamJIeKQKkZ+mxpohl cloud.google.com/go/storage v1.8.0/go.mod h1:Wv1Oy7z6Yz3DshWRJFhqM/UCfaWIRTdp0RXyy7KQOVs= cloud.google.com/go/storage v1.10.0/go.mod h1:FLPqc6j+Ki4BU591ie1oL6qBQGu2Bl/tZ9ullr3+Kg0= cloud.google.com/go/storage v1.14.0/go.mod h1:GrKmX003DSIwi9o29oFT7YDnHYwZoctc3fOKtUw0Xmo= +dario.cat/mergo v1.0.0 h1:AGCNq9Evsj31mOgNPcLyXc+4PNABt905YmuqPYYpBWk= +dario.cat/mergo v1.0.0/go.mod h1:uNxQE+84aUszobStD9th8a29P2fMDhsBdgRYvZOxGmk= dmitri.shuralyov.com/gpu/mtl v0.0.0-20190408044501-666a987793e9/go.mod h1:H6x//7gZCb22OMCxBHrMx7a5I7Hp++hsVxbQ4BYO7hU= github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU= github.com/BurntSushi/xgb v0.0.0-20160522181843-27f122750802/go.mod h1:IVnqGOEym/WlBOVXweHU+Q+/VP0lqqI8lqeDx9IjBqo= -github.com/OneOfOne/xxhash v1.2.2/go.mod h1:HSdplMjZKSmBqAxg5vPj2TmRDmfkzw+cTzAElWljhcU= -github.com/Shopify/sarama v1.32.0 h1:P+RUjEaRU0GMMbYexGMDyrMkLhbbBVUVISDywi+IlFU= -github.com/Shopify/sarama v1.32.0/go.mod h1:+EmJJKZWVT/faR9RcOxJerP+LId4iWdQPBGLy1Y1Njs= -github.com/Shopify/toxiproxy/v2 v2.3.0 h1:62YkpiP4bzdhKMH+6uC5E95y608k3zDwdzuBMsnn3uQ= -github.com/Shopify/toxiproxy/v2 v2.3.0/go.mod h1:KvQTtB6RjCJY4zqNJn7C7JDFgsG5uoHYDirfUfpIm0c= -github.com/ThreeDotsLabs/watermill v1.1.1 h1:+9NXqWQvplzxBru2CIInvVOZeKUnM+Nysg42fInl5sY= -github.com/ThreeDotsLabs/watermill v1.1.1/go.mod h1:Qd1xNFxolCAHCzcMrm6RnjW0manbvN+DJVWc1MWRFlI= -github.com/ThreeDotsLabs/watermill-kafka/v2 v2.2.2 h1:COB5neqVL8jGwoz1Y9dawQ7Xhxid1XXX8+1CI/PebVU= -github.com/ThreeDotsLabs/watermill-kafka/v2 v2.2.2/go.mod h1:U001oyrHo+df3Q7hIXgKqxY2OW6woz64+GNuIxZokbM= -github.com/alecthomas/template v0.0.0-20160405071501-a0175ee3bccc/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc= -github.com/alecthomas/template v0.0.0-20190718012654-fb15b899a751/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc= -github.com/alecthomas/units v0.0.0-20151022065526-2efee857e7cf/go.mod h1:ybxpYRFXyAe+OPACYpWeL0wqObRcbAqCMya13uyzqw0= -github.com/alecthomas/units v0.0.0-20190717042225-c3de453c63f4/go.mod h1:ybxpYRFXyAe+OPACYpWeL0wqObRcbAqCMya13uyzqw0= -github.com/alecthomas/units v0.0.0-20190924025748-f65c72e2690d/go.mod h1:rBZYJk541a8SKzHPHnH3zbiI+7dagKZ0cgpgrD7Fyho= -github.com/antihax/optional v1.0.0/go.mod h1:uupD/76wgC+ih3iEmQUL+0Ugr19nfwCT1kdvxnR2qWY= -github.com/beorn7/perks v0.0.0-20180321164747-3a771d992973/go.mod h1:Dwedo/Wpr24TaqPxmxbtue+5NUziq4I4S80YR8gNf3Q= -github.com/beorn7/perks v1.0.0/go.mod h1:KWe93zE9D1o94FZ5RNwFwVgaQK1VOXiVxmqh+CedLV8= -github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM= -github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw= -github.com/blang/semver v3.5.1+incompatible h1:cQNTCjp13qL8KC3Nbxr/y2Bqb63oX6wdnnjpJbkM4JQ= -github.com/blang/semver v3.5.1+incompatible/go.mod h1:kRBLl5iJ+tD4TcOOxsy/0fnwebNt5EWlYSAyrTnjyyk= -github.com/buger/jsonparser v1.1.1/go.mod h1:6RYKKt7H4d4+iWqouImQ9R2FZql3VbhNgx27UK13J/0= -github.com/cenkalti/backoff/v3 v3.0.0 h1:ske+9nBpD9qZsTBoF41nW5L+AIuFBKMeze18XQ3eG1c= -github.com/cenkalti/backoff/v3 v3.0.0/go.mod h1:cIeZDE3IrqwwJl6VUwCN6trj1oXrTS4rc0ij+ULvLYs= +github.com/Microsoft/go-winio v0.6.2 h1:F2VQgta7ecxGYO8k3ZZz3RS8fVIXVxONVUPlNERoyfY= +github.com/Microsoft/go-winio v0.6.2/go.mod h1:yd8OoFMLzJbo9gZq8j5qaps8bJ9aShtEA8Ipt1oGCvU= +github.com/ProtonMail/go-crypto v1.3.0 h1:ILq8+Sf5If5DCpHQp4PbZdS1J7HDFRXz/+xKBiRGFrw= +github.com/ProtonMail/go-crypto v1.3.0/go.mod h1:9whxjD8Rbs29b4XWbB8irEcE8KHMqaR2e7GWU1R+/PE= +github.com/apparentlymart/go-textseg/v15 v15.0.0 h1:uYvfpb3DyLSCGWnctWKGj857c6ew1u1fNQOlOtuGxQY= +github.com/apparentlymart/go-textseg/v15 v15.0.0/go.mod h1:K8XmNZdhEBkdlyDdvbmmsvpAG721bKi0joRfFdHIWJ4= +github.com/bsm/ginkgo/v2 v2.12.0 h1:Ny8MWAHyOepLGlLKYmXG4IEkioBysk6GpaRTLC8zwWs= +github.com/bsm/ginkgo/v2 v2.12.0/go.mod h1:SwYbGRRDovPVboqFv0tPTcG1sN61LM1Z4ARdbAV9g4c= +github.com/bsm/gomega v1.27.10 h1:yeMWxP2pV2fG3FgAODIY8EiRE3dy0aeFYt4l7wh6yKA= +github.com/bsm/gomega v1.27.10/go.mod h1:JyEr/xRbxbtgWNi8tIEVPUYZ5Dzef52k01W3YH0H+O0= +github.com/cenkalti/backoff/v4 v4.3.0 h1:MyRJ/UdXutAwSAT+s3wNd7MfTIcy71VQueUuFK343L8= +github.com/cenkalti/backoff/v4 v4.3.0/go.mod h1:Y3VNntkOUPxTVeUxJ/G5vcM//AlwfmyYozVcomhLiZE= github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU= -github.com/cespare/xxhash v1.1.0/go.mod h1:XrSqR1VqqWfGrhpAt58auRo0WTKS1nRRg3ghfAqPWnc= -github.com/cespare/xxhash/v2 v2.1.1/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= -github.com/cespare/xxhash/v2 v2.1.2 h1:YRXhKfTDauu4ajMg1TPgFO5jnlC2HCbmLXMcTG5cbYE= -github.com/cespare/xxhash/v2 v2.1.2/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= +github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs= +github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= github.com/chzyer/logex v1.1.10/go.mod h1:+Ywpsq7O8HXn0nuIou7OrIPyXbp3wmkHB+jjWRnGsAI= github.com/chzyer/readline v0.0.0-20180603132655-2972be24d48e/go.mod h1:nSuG5e5PlCu98SY8svDHJxuZscDgtXS6KTTbou5AhLI= github.com/chzyer/test v0.0.0-20180213035817-a1ea475d72b1/go.mod h1:Q3SI9o4m/ZMnBNeIyt5eFwwo7qiLfzFZmjNmxjkiQlU= github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw= +github.com/cloudflare/circl v1.6.1 h1:zqIqSPIndyBh1bjLVVDHMPpVKqp8Su/V+6MeDzzQBQ0= +github.com/cloudflare/circl v1.6.1/go.mod h1:uddAzsPgqdMAYatqJ0lsjX1oECcQLIlRpzZh3pJrofs= github.com/cncf/udpa/go v0.0.0-20191209042840-269d4d468f6f/go.mod h1:M8M6+tZqaGXZJjfX53e64911xZQV5JYwmTeXPW+k8Sc= github.com/cncf/udpa/go v0.0.0-20200629203442-efcf912fb354/go.mod h1:WmhPx2Nbnhtbo57+VJT5O0JRkEi1Wbu0z5j0R8u5Hbk= github.com/cncf/udpa/go v0.0.0-20201120205902-5459f2c99403/go.mod h1:WmhPx2Nbnhtbo57+VJT5O0JRkEi1Wbu0z5j0R8u5Hbk= -github.com/cncf/xds/go v0.0.0-20210312221358-fbca930ec8ed/go.mod h1:eXthEFrGJvWHgFFCl3hGmgk+/aYT6PnTQLykKQRLhEs= -github.com/cpuguy83/go-md2man/v2 v2.0.0-20190314233015-f79a8a8ca69d/go.mod h1:maD7wRr/U5Z6m/iR4s+kqSMx2CaBsrgA7czyZG/E6dU= -github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E= +github.com/cyphar/filepath-securejoin v0.4.1 h1:JyxxyPEaktOD+GAnqIqTf9A8tHyAG22rowi7HkoSU1s= +github.com/cyphar/filepath-securejoin v0.4.1/go.mod h1:Sdj7gXlvMcPZsbhwhQ33GguGLDGQL7h7bg04C/+u9jI= github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= -github.com/docopt/docopt-go v0.0.0-20180111231733-ee0de3bc6815/go.mod h1:WwZ+bS3ebgob9U8Nd0kOddGdZWjyMGR8Wziv+TBNwSE= -github.com/eapache/go-resiliency v1.2.0 h1:v7g92e/KSN71Rq7vSThKaWIq68fL4YHvWyiUKorFR1Q= -github.com/eapache/go-resiliency v1.2.0/go.mod h1:kFI+JgMyC7bLPUVY133qvEBtVayf5mFgVsvEsIPBvNs= -github.com/eapache/go-xerial-snappy v0.0.0-20180814174437-776d5712da21 h1:YEetp8/yCZMuEPMUDHG0CW/brkkEp8mzqk2+ODEitlw= -github.com/eapache/go-xerial-snappy v0.0.0-20180814174437-776d5712da21/go.mod h1:+020luEh2TKB4/GOp8oxxtq0Daoen/Cii55CzbTV6DU= -github.com/eapache/queue v1.1.0 h1:YOEu7KNc61ntiQlcEeUIoDTJ2o8mQznoNvUhiigpIqc= -github.com/eapache/queue v1.1.0/go.mod h1:6eCeP0CKFpHLu8blIFXhExK/dRa7WDZfr6jVFPTqq+I= -github.com/emicklei/go-restful/v3 v3.9.0 h1:XwGDlfxEnQZzuopoqxwSEllNcCOM9DhhFyhFIIGKwxE= -github.com/emicklei/go-restful/v3 v3.9.0/go.mod h1:6n3XBCmQQb25CM2LCACGz8ukIrRry+4bhvbpWn3mrbc= +github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f h1:lO4WD4F/rVNCu3HqELle0jiPLLBs70cWOduZpkS1E78= +github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f/go.mod h1:cuUVRXasLTGF7a8hSLbxyZXjz+1KgoB3wDUb6vlszIc= +github.com/emirpasic/gods v1.18.1 h1:FXtiHYKDGKCW2KzwZKx0iC0PQmdlorYgdFG9jPXJ1Bc= +github.com/emirpasic/gods v1.18.1/go.mod h1:8tpGGwCnJ5H4r6BWwaV6OrWmMoPhUl5jm/FMNAnJvWQ= github.com/envoyproxy/go-control-plane v0.9.0/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4= github.com/envoyproxy/go-control-plane v0.9.1-0.20191026205805-5f8ba28d4473/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4= github.com/envoyproxy/go-control-plane v0.9.4/go.mod h1:6rpuAdCZL397s3pYoYcLgu1mIlRU8Am5FuJP05cCM98= github.com/envoyproxy/go-control-plane v0.9.7/go.mod h1:cwu0lG7PUMfa9snN8LXBig5ynNVH9qI8YYLbd1fK2po= github.com/envoyproxy/go-control-plane v0.9.9-0.20201210154907-fd9021fe5dad/go.mod h1:cXg6YxExXjJnVBQHBLXeUAgxn2UodCpnH306RInaBQk= -github.com/envoyproxy/go-control-plane v0.9.9-0.20210512163311-63b5d3c536b0/go.mod h1:hliV/p42l8fGbc6Y9bQ70uLwIvmJyVE5k4iMKlh8wCQ= github.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7+kN2VEUnK/pcBlmesArF7c= -github.com/evanphx/json-patch v0.5.2/go.mod h1:ZWS5hhDbVDyob71nXKNL0+PWn6ToqBHMikGIFbs31qQ= -github.com/evanphx/json-patch v5.6.0+incompatible h1:jBYDEEiFBPxA0v50tFdvOzQQTCvpL6mnFh5mB2/l16U= -github.com/evanphx/json-patch/v5 v5.6.0 h1:b91NhWfaz02IuVxO9faSllyAtNXHMPkC5J8sJCLunww= -github.com/evanphx/json-patch/v5 v5.6.0/go.mod h1:G79N1coSVB93tBe7j6PhzjmR3/2VvlbKOFpnXhI9Bw4= -github.com/flowstack/go-jsonschema v0.1.1/go.mod h1:yL7fNggx1o8rm9RlgXv7hTBWxdBM0rVwpMwimd3F3N0= -github.com/fortytw2/leaktest v1.3.0 h1:u8491cBMTQ8ft8aeV+adlcytMZylmA5nnwwkRZjI8vw= -github.com/fortytw2/leaktest v1.3.0/go.mod h1:jDsjWgpAGjm2CA7WthBh/CdZYEPF31XHquHwclZch5g= -github.com/frankban/quicktest v1.14.2/go.mod h1:mgiwOwqx65TmIk1wJ6Q7wvnVMocbUorkibMOrVTHZps= github.com/frankban/quicktest v1.14.3 h1:FJKSZTDHjyhriyC81FLQ0LY93eSai0ZyR/ZIkd3ZUKE= +github.com/frankban/quicktest v1.14.3/go.mod h1:mgiwOwqx65TmIk1wJ6Q7wvnVMocbUorkibMOrVTHZps= github.com/fsnotify/fsnotify v1.6.0 h1:n+5WquG0fcWoWp6xPWfHdbskMCQaFnG6PfBrh1Ky4HY= github.com/fsnotify/fsnotify v1.6.0/go.mod h1:sl3t1tCWJFWoRz9R8WJCbQihKKwmorjAbSClcnxKAGw= -github.com/ghodss/yaml v1.0.0/go.mod h1:4dBDuWmgqj2HViK6kFavaiC9ZROes6MMH2rRYeMEF04= -github.com/go-chi/chi v4.0.2+incompatible/go.mod h1:eB3wogJHnLi3x/kFX2A+IbTBlXxmMeXJVKy9tTv1XzQ= +github.com/go-git/gcfg v1.5.1-0.20230307220236-3a3c6141e376 h1:+zs/tPmkDkHx3U66DAb0lQFJrpS6731Oaa12ikc+DiI= +github.com/go-git/gcfg v1.5.1-0.20230307220236-3a3c6141e376/go.mod h1:an3vInlBmSxCcxctByoQdvwPiA7DTK7jaaFDBTtu0ic= +github.com/go-git/go-billy/v5 v5.6.2 h1:6Q86EsPXMa7c3YZ3aLAQsMA0VlWmy43r6FHqa/UNbRM= +github.com/go-git/go-billy/v5 v5.6.2/go.mod h1:rcFC2rAsp/erv7CMz9GczHcuD0D32fWzH+MJAU+jaUU= +github.com/go-git/go-git/v5 v5.16.5 h1:mdkuqblwr57kVfXri5TTH+nMFLNUxIj9Z7F5ykFbw5s= +github.com/go-git/go-git/v5 v5.16.5/go.mod h1:QOMLpNf1qxuSY4StA/ArOdfFR2TrKEjJiye2kel2m+M= github.com/go-gl/glfw v0.0.0-20190409004039-e6da0acd62b1/go.mod h1:vR7hzQXu2zJy9AVAgeJqvqgH9Q5CA+iKCZ2gyEVpxRU= github.com/go-gl/glfw/v3.3/glfw v0.0.0-20191125211704-12ad95a8df72/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8= github.com/go-gl/glfw/v3.3/glfw v0.0.0-20200222043503-6f7a984d4dc4/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8= -github.com/go-kit/kit v0.8.0/go.mod h1:xBxKIO96dXMWWy0MnWVtmwkA9/13aqxPnvrjFYMA2as= -github.com/go-kit/kit v0.9.0/go.mod h1:xBxKIO96dXMWWy0MnWVtmwkA9/13aqxPnvrjFYMA2as= -github.com/go-kit/log v0.1.0/go.mod h1:zbhenjAZHb184qTLMA9ZjW7ThYL0H2mk7Q6pNt4vbaY= -github.com/go-kit/log v0.2.0/go.mod h1:NwTd00d/i8cPZ3xOwwiv2PO5MOcx78fFErGNcVmBjv0= -github.com/go-logfmt/logfmt v0.3.0/go.mod h1:Qt1PoO58o5twSAckw1HlFXLmHsOX5/0LbT9GBnD5lWE= -github.com/go-logfmt/logfmt v0.4.0/go.mod h1:3RMwSq7FuexP4Kalkev3ejPJsZTpXXBr9+V4qmtdjCk= -github.com/go-logfmt/logfmt v0.5.0/go.mod h1:wCYkCAKZfumFQihp8CzCvQ3paCTfi41vtzG1KdI/P7A= -github.com/go-logfmt/logfmt v0.5.1/go.mod h1:WYhtIu8zTZfxdn5+rREduYbwxfcBr/Vr6KEVveWlfTs= -github.com/go-logr/logr v1.2.0/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A= github.com/go-logr/logr v1.2.2/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A= -github.com/go-logr/logr v1.2.3 h1:2DntVwHkVopvECVRSlL5PSo9eG+cAkDCuckLubN+rq0= -github.com/go-logr/logr v1.2.3/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A= +github.com/go-logr/logr v1.4.3 h1:CjnDlHq8ikf6E492q6eKboGOC0T8CDaOvkHCIg8idEI= +github.com/go-logr/logr v1.4.3/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY= github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag= github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE= -github.com/go-logr/zapr v1.2.3 h1:a9vnzlIBPQBBkeaR9IuMUfmVOrQlkoC4YfPoFkX3T7A= -github.com/go-openapi/jsonpointer v0.19.6 h1:eCs3fxoIi3Wh6vtgmLTOjdhSpiqphQ+DaPn38N2ZdrE= -github.com/go-openapi/jsonpointer v0.19.6/go.mod h1:osyAmYz/mB/C3I+WsTTSgw1ONzaLJoLCyoi6/zppojs= -github.com/go-openapi/jsonreference v0.20.1 h1:FBLnyygC4/IZZr893oiomc9XaghoveYTrLC1F86HID8= -github.com/go-openapi/jsonreference v0.20.1/go.mod h1:Bl1zwGIM8/wsvqjsOQLJ/SH+En5Ap4rVB5KVcIDZG2k= -github.com/go-openapi/swag v0.22.3 h1:yMBqmnQ0gyZvEb/+KzuWZOXgllrXT4SADYbvDaXHv/g= -github.com/go-openapi/swag v0.22.3/go.mod h1:UzaqsxGiab7freDnrUUra0MwWfN/q7tE4j+VcZ0yl14= -github.com/go-stack/stack v1.8.0/go.mod h1:v0f6uXyyMGvRgIKkXu+yp6POWl0qKG85gN/melR3HDY= -github.com/go-task/slim-sprig v0.0.0-20210107165309-348f09dbbbc0 h1:p104kn46Q8WdvHunIJ9dAyjPVtrBPhSr3KT2yUst43I= -github.com/gogo/protobuf v1.1.1/go.mod h1:r8qH/GZQm5c6nD/R0oafs1akxWv10x8SbQlK7atdtwQ= -github.com/gogo/protobuf v1.2.1/go.mod h1:hp+jE20tsWTFYpLwKvXlhS1hjn+gTNwPg2I6zVXpSg4= -github.com/gogo/protobuf v1.3.2 h1:Ov1cvc58UF3b5XjBnZv7+opcTcQFZebYjWzi34vdm4Q= -github.com/gogo/protobuf v1.3.2/go.mod h1:P1XiOD3dCwIKUDQYPy72D8LYyHL2YPYrpS2s69NZV8Q= github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q= github.com/golang/groupcache v0.0.0-20190702054246-869f871628b6/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= github.com/golang/groupcache v0.0.0-20191227052852-215e87163ea7/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= github.com/golang/groupcache v0.0.0-20200121045136-8c9f03a8e57e/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= -github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da h1:oI5xCqsCo564l8iNU+DwB5epxmsaqB+rhGL0m5jtYqE= -github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= +github.com/golang/groupcache v0.0.0-20241129210726-2c02b8208cf8 h1:f+oWsMOmNPc8JmEHVZIycC7hBoQxHH9pNKQORJNozsQ= +github.com/golang/groupcache v0.0.0-20241129210726-2c02b8208cf8/go.mod h1:wcDNUvekVysuuOpQKo3191zZyTpiI6se1N1ULghS0sw= github.com/golang/mock v1.1.1/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A= github.com/golang/mock v1.2.0/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A= github.com/golang/mock v1.3.1/go.mod h1:sBzyDLLjw3U8JLTeZvSv8jJB+tU5PVekmnlKIyFUx0Y= @@ -166,17 +124,11 @@ github.com/golang/protobuf v1.4.0/go.mod h1:jodUvKwWbYaEsadDk5Fwe5c77LiNKVO9IDvq github.com/golang/protobuf v1.4.1/go.mod h1:U8fpvMrcmy5pZrNK1lt4xCsGvpyWQ/VVv6QDs8UjoX8= github.com/golang/protobuf v1.4.2/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI= github.com/golang/protobuf v1.4.3/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI= -github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk= -github.com/golang/protobuf v1.5.2/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY= -github.com/golang/protobuf v1.5.3 h1:KhyjKVUg7Usr/dYsdSqoFveMYd5ko72D+zANwlG1mmg= -github.com/golang/protobuf v1.5.3/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY= github.com/golang/snappy v0.0.1/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q= github.com/golang/snappy v0.0.4 h1:yAGX7huGHXlcLOEtBnF4w7FQwA26wojNCwOYAEhLjQM= github.com/golang/snappy v0.0.4/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q= github.com/google/btree v0.0.0-20180813153112-4030bb1f1f0c/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ= github.com/google/btree v1.0.0/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ= -github.com/google/gnostic v0.6.9 h1:ZK/5VhkoX835RikCHpSUJV9a+S3e1zLh59YnyWeBW+0= -github.com/google/gnostic v0.6.9/go.mod h1:Nm8234We1lq6iB9OmlgNv3nH91XLLVZHCDayfA3xq+E= github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M= github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= @@ -186,13 +138,8 @@ github.com/google/go-cmp v0.5.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/ github.com/google/go-cmp v0.5.1/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= github.com/google/go-cmp v0.5.2/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= github.com/google/go-cmp v0.5.4/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= -github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= -github.com/google/go-cmp v0.5.7/go.mod h1:n+brtR0CgQNWTVd5ZUFpTBC8YFBDLK/h/bpaJ8/DtOE= -github.com/google/go-cmp v0.5.9 h1:O2Tfq5qg4qc4AmwVlvv0oLiVAGB7enBSJ2x2DqQFi38= -github.com/google/go-cmp v0.5.9/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= -github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= -github.com/google/gofuzz v1.2.0 h1:xRy4A+RhZaiKjJ1bPfwQ8sedCA+YS2YcCHW6ec7JMi0= -github.com/google/gofuzz v1.2.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= +github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8= +github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU= github.com/google/martian v2.1.0+incompatible/go.mod h1:9I4somxYTbIHy5NJKHRl3wXiIaQGbYVAs8BPL6v8lEs= github.com/google/martian/v3 v3.0.0/go.mod h1:y5Zk1BBys9G+gd6Jrk0W3cC1+ELVxBWuIGO+w/tUAp0= github.com/google/martian/v3 v3.1.0/go.mod h1:y5Zk1BBys9G+gd6Jrk0W3cC1+ELVxBWuIGO+w/tUAp0= @@ -206,163 +153,80 @@ github.com/google/pprof v0.0.0-20200708004538-1a94d8640e99/go.mod h1:ZgVRPoUq/hf github.com/google/pprof v0.0.0-20201023163331-3e6fc7fc9c4c/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE= github.com/google/pprof v0.0.0-20201203190320-1bf35d6f28c2/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE= github.com/google/pprof v0.0.0-20201218002935-b9804c9f04c2/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE= -github.com/google/pprof v0.0.0-20210720184732-4bb14d4b1be1 h1:K6RDEckDVWvDI9JAJYCmNdQXq6neHJOYx3V6jnqNEec= github.com/google/renameio v0.1.0/go.mod h1:KWCgfxg9yswjAJkECMjeO8J8rahYeXnNhOm40UhjYkI= -github.com/google/uuid v1.1.1/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= github.com/google/uuid v1.1.2/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= -github.com/google/uuid v1.2.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= -github.com/google/uuid v1.3.0 h1:t6JiXgmwXMjEs8VusXIJk2BXHsn+wx8BZdTaoZ5fu7I= -github.com/google/uuid v1.3.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= +github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= +github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= github.com/googleapis/gax-go/v2 v2.0.4/go.mod h1:0Wqv26UfaUD9n4G6kQubkQ+KchISgw+vpHVxEJEs9eg= github.com/googleapis/gax-go/v2 v2.0.5/go.mod h1:DWXyrwAJ9X0FpwwEdw+IPEYBICEFu5mhpdKc/us6bOk= github.com/googleapis/google-cloud-go-testing v0.0.0-20200911160855-bcd43fbb19e8/go.mod h1:dvDLG8qkwmyD9a/MJJN3XJcT3xFxOKAvTZGvuZmac9g= -github.com/gorilla/mux v1.8.0/go.mod h1:DVbg23sWSpFRCP0SfiEN6jmj59UnW/n46BH5rLB71So= -github.com/gorilla/securecookie v1.1.1/go.mod h1:ra0sb63/xPlUeL+yeDciTfxMRAA+MP+HVt/4epWDjd4= -github.com/gorilla/sessions v1.2.1/go.mod h1:dk2InVEVJ0sfLlnXv9EAgkf6ecYs/i80K/zI+bUmuGM= -github.com/grpc-ecosystem/grpc-gateway v1.16.0/go.mod h1:BDjrQk3hbvj6Nolgz8mAMFbcEtjT1g+wF4CSlocrBnw= -github.com/hashicorp/errwrap v1.0.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4= -github.com/hashicorp/errwrap v1.1.0 h1:OxrOeh75EUXMY8TBjag2fzXGZ40LB6IKw45YeGUDY2I= -github.com/hashicorp/errwrap v1.1.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4= -github.com/hashicorp/go-multierror v1.0.0/go.mod h1:dHtQlpGsu+cZNNAkkCN/P3hoUDHhCYQXV3UM06sGGrk= -github.com/hashicorp/go-multierror v1.1.1 h1:H5DkEtf6CXdFp0N0Em5UCwQpXMWke8IA0+lD48awMYo= -github.com/hashicorp/go-multierror v1.1.1/go.mod h1:iw975J/qwKPdAO1clOe2L8331t/9/fmwbPZ6JB6eMoM= -github.com/hashicorp/go-uuid v1.0.2 h1:cfejS+Tpcp13yd5nYHWDI6qVCny6wyX2Mt5SGur2IGE= -github.com/hashicorp/go-uuid v1.0.2/go.mod h1:6SBZvOh/SIDV7/2o3Jml5SYk/TvGqwFJ/bN7x4byOro= +github.com/grpc-ecosystem/grpc-gateway/v2 v2.23.0 h1:ad0vkEBuk23VJzZR9nkLVG0YAoN9coASF1GusYX6AlU= +github.com/grpc-ecosystem/grpc-gateway/v2 v2.23.0/go.mod h1:igFoXX2ELCW06bol23DWPB5BEWfZISOzSP5K2sbLea0= +github.com/hashicorp/go-cleanhttp v0.5.2 h1:035FKYIWjmULyFRBKPs8TBQoi0x6d9G4xc9neXJWAZQ= +github.com/hashicorp/go-cleanhttp v0.5.2/go.mod h1:kO/YDlP8L1346E6Sodw+PrpBSV4/SoxCXGY6BqNFT48= +github.com/hashicorp/go-retryablehttp v0.7.8 h1:ylXZWnqa7Lhqpk0L1P1LzDtGcCR0rPVUrx/c8Unxc48= +github.com/hashicorp/go-retryablehttp v0.7.8/go.mod h1:rjiScheydd+CxvumBsIrFKlx3iS0jrZ7LvzFGFmuKbw= +github.com/hashicorp/go-version v1.8.0 h1:KAkNb1HAiZd1ukkxDFGmokVZe1Xy9HG6NUp+bPle2i4= +github.com/hashicorp/go-version v1.8.0/go.mod h1:fltr4n8CU8Ke44wwGCBoEymUuxUHl09ZGVZPK5anwXA= github.com/hashicorp/golang-lru v0.5.0/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8= github.com/hashicorp/golang-lru v0.5.1/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8= +github.com/hashicorp/hc-install v0.9.3 h1:1H4dgmgzxEVwT6E/d/vIL5ORGVKz9twRwDw+qA5Hyho= +github.com/hashicorp/hc-install v0.9.3/go.mod h1:FQlQ5I3I/X409N/J1U4pPeQQz1R3BoV0IysB7aiaQE0= github.com/hashicorp/hcl v1.0.0 h1:0Anlzjpi4vEasTeNFn2mLJgTSwt0+6sfsiTG8qcWGx4= github.com/hashicorp/hcl v1.0.0/go.mod h1:E5yfLk+7swimpb2L/Alb/PJmXilQ/rhwaUYs4T20WEQ= +github.com/hashicorp/terraform-exec v0.25.0 h1:Bkt6m3VkJqYh+laFMrWIpy9KHYFITpOyzRMNI35rNaY= +github.com/hashicorp/terraform-exec v0.25.0/go.mod h1:dl9IwsCfklDU6I4wq9/StFDp7dNbH/h5AnfS1RmiUl8= +github.com/hashicorp/terraform-json v0.27.2 h1:BwGuzM6iUPqf9JYM/Z4AF1OJ5VVJEEzoKST/tRDBJKU= +github.com/hashicorp/terraform-json v0.27.2/go.mod h1:GzPLJ1PLdUG5xL6xn1OXWIjteQRT2CNT9o/6A9mi9hE= github.com/ianlancetaylor/demangle v0.0.0-20181102032728-5e5cf60278f6/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc= github.com/ianlancetaylor/demangle v0.0.0-20200824232613-28f6c0f3b639/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc= -github.com/imdario/mergo v0.3.13 h1:lFzP57bqS/wsqKssCGmtLAb8A0wKjLGrve2q3PPVcBk= -github.com/imdario/mergo v0.3.13/go.mod h1:4lJ1jqUDcsbIECGy0RUJAXNIhg+6ocWgb1ALK2O4oXg= -github.com/jcmturner/aescts/v2 v2.0.0 h1:9YKLH6ey7H4eDBXW8khjYslgyqG2xZikXP0EQFKrle8= -github.com/jcmturner/aescts/v2 v2.0.0/go.mod h1:AiaICIRyfYg35RUkr8yESTqvSy7csK90qZ5xfvvsoNs= -github.com/jcmturner/dnsutils/v2 v2.0.0 h1:lltnkeZGL0wILNvrNiVCR6Ro5PGU/SeBvVO/8c/iPbo= -github.com/jcmturner/dnsutils/v2 v2.0.0/go.mod h1:b0TnjGOvI/n42bZa+hmXL+kFJZsFT7G4t3HTlQ184QM= -github.com/jcmturner/gofork v1.0.0 h1:J7uCkflzTEhUZ64xqKnkDxq3kzc96ajM1Gli5ktUem8= -github.com/jcmturner/gofork v1.0.0/go.mod h1:MK8+TM0La+2rjBD4jE12Kj1pCCxK7d2LK/UM3ncEo0o= -github.com/jcmturner/goidentity/v6 v6.0.1 h1:VKnZd2oEIMorCTsFBnJWbExfNN7yZr3EhJAxwOkZg6o= -github.com/jcmturner/goidentity/v6 v6.0.1/go.mod h1:X1YW3bgtvwAXju7V3LCIMpY0Gbxyjn/mY9zx4tFonSg= -github.com/jcmturner/gokrb5/v8 v8.4.2 h1:6ZIM6b/JJN0X8UM43ZOM6Z4SJzla+a/u7scXFJzodkA= -github.com/jcmturner/gokrb5/v8 v8.4.2/go.mod h1:sb+Xq/fTY5yktf/VxLsE3wlfPqQjp0aWNYyvBVK62bc= -github.com/jcmturner/rpc/v2 v2.0.3 h1:7FXXj8Ti1IaVFpSAziCZWNzbNuZmnvw/i6CqLNdWfZY= -github.com/jcmturner/rpc/v2 v2.0.3/go.mod h1:VUJYCIDm3PVOEHw8sgt091/20OJjskO/YJki3ELg/Hc= -github.com/jessevdk/go-flags v1.4.0/go.mod h1:4FA24M0QyGHXBuZZK/XkWh8h0e1EYbRYJSGM75WSRxI= -github.com/josharian/intern v1.0.0 h1:vlS4z54oSdjm0bgjRigI+G1HpF+tI+9rE5LLzOg8HmY= -github.com/josharian/intern v1.0.0/go.mod h1:5DoeVV0s6jJacbCEi61lwdGj/aVlrQvzHFFd8Hwg//Y= -github.com/jpillora/backoff v1.0.0/go.mod h1:J/6gKK9jxlEcS3zixgDgUAsiuZ7yrSoa/FX5e0EB2j4= -github.com/json-iterator/go v1.1.6/go.mod h1:+SdeFBvtyEkXs7REEP0seUULqWtbJapLOCVDaaPEHmU= -github.com/json-iterator/go v1.1.10/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4= -github.com/json-iterator/go v1.1.11/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4= -github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM= -github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo= +github.com/jbenet/go-context v0.0.0-20150711004518-d14ea06fba99 h1:BQSFePA1RWJOlocH6Fxy8MmwDt+yVQYULKfN0RoTN8A= +github.com/jbenet/go-context v0.0.0-20150711004518-d14ea06fba99/go.mod h1:1lJo3i6rXxKeerYnT8Nvf0QmHCRC1n8sfWVwXF2Frvo= github.com/jstemmer/go-junit-report v0.0.0-20190106144839-af01ea7f8024/go.mod h1:6v2b51hI/fHJwM22ozAgKL4VKDeJcHhJFhtBdhmNjmU= github.com/jstemmer/go-junit-report v0.9.1/go.mod h1:Brl9GWCQeLvo8nXZwPNNblvFj/XSXhF0NWZEnDohbsk= -github.com/julienschmidt/httprouter v1.2.0/go.mod h1:SYymIcj16QtmaHHD7aYtjjsJG7VTCxuUUipMqKk8s4w= -github.com/julienschmidt/httprouter v1.3.0/go.mod h1:JR6WtHb+2LUe8TCKY3cZOxFyyO8IZAc4RVcycCCAKdM= -github.com/kisielk/errcheck v1.1.0/go.mod h1:EZBBE59ingxPouuu3KfxchcWSUPOHkagtvWXihfKN4Q= -github.com/kisielk/errcheck v1.5.0/go.mod h1:pFxgyoBC7bSaBwPgfKdkLd5X25qrDl4LWUI2bnpBCr8= +github.com/kevinburke/ssh_config v1.2.0 h1:x584FjTGwHzMwvHx18PXxbBVzfnxogHaAReU4gf13a4= +github.com/kevinburke/ssh_config v1.2.0/go.mod h1:CT57kijsi8u/K/BOFA39wgDQJ9CxiF4nAY/ojJ6r6mM= github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck= github.com/klauspost/compress v1.13.6/go.mod h1:/3/Vjq9QcHkK5uEr5lBEmyoZ1iFhe47etQ6QUkpK6sk= -github.com/klauspost/compress v1.14.4/go.mod h1:/3/Vjq9QcHkK5uEr5lBEmyoZ1iFhe47etQ6QUkpK6sk= github.com/klauspost/compress v1.15.1 h1:y9FcTHGyrebwfP0ZZqFiaxTaiDnUrGkJkI+f583BL1A= github.com/klauspost/compress v1.15.1/go.mod h1:/3/Vjq9QcHkK5uEr5lBEmyoZ1iFhe47etQ6QUkpK6sk= -github.com/konsorten/go-windows-terminal-sequences v1.0.1/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ= -github.com/konsorten/go-windows-terminal-sequences v1.0.3/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ= +github.com/klauspost/cpuid/v2 v2.0.9 h1:lgaqFMSdTdQYdZ04uHyN2d/eKdOMyi2YLSvlQIBFYa4= +github.com/klauspost/cpuid/v2 v2.0.9/go.mod h1:FInQzS24/EEf25PyTYn52gqo7WaD8xa0213Md/qVLRg= github.com/kr/fs v0.1.0/go.mod h1:FFnZGqtBN9Gxj7eW1uZ42v5BccTP0vu6NEaFoC2HwRg= -github.com/kr/logfmt v0.0.0-20140226030751-b84e30acd515/go.mod h1:+0opPa2QZZtGFBFZlji/RkVcI2GknAs/DXo4wKdlNEc= github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= -github.com/kr/pretty v0.2.0/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI= -github.com/kr/pretty v0.2.1/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI= -github.com/kr/pretty v0.3.0 h1:WgNl7dwNpEZ6jJ9k1snq4pZsg7DOEN8hP9Xw0Tsjwk0= -github.com/kr/pretty v0.3.0/go.mod h1:640gp4NfQd8pI5XOwp5fnNeVWj67G7CFk/SaSQn7NBk= +github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE= +github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk= github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= -github.com/lithammer/shortuuid/v3 v3.0.4/go.mod h1:RviRjexKqIzx/7r1peoAITm6m7gnif/h+0zmolKJjzw= -github.com/lithammer/shortuuid/v3 v3.0.7 h1:trX0KTHy4Pbwo/6ia8fscyHoGA+mf1jWbPJVuvyJQQ8= -github.com/lithammer/shortuuid/v3 v3.0.7/go.mod h1:vMk8ke37EmiewwolSO1NLW8vP4ZaKlRuDIi8tWWmAts= github.com/magiconair/properties v1.8.6 h1:5ibWZ6iY0NctNGWo87LalDlEZ6R41TqbbDamhfG/Qzo= github.com/magiconair/properties v1.8.6/go.mod h1:y3VJvCyxH9uVvJTWEGAELF3aiYNyPKd5NZ3oSwXrF60= -github.com/mailru/easyjson v0.7.7 h1:UGYAvKxe3sBsEDzO8ZeWOSlIQfWFlxbzLZe7hwFURr0= -github.com/mailru/easyjson v0.7.7/go.mod h1:xzfreul335JAWq5oZzymOObrkdz5UnU4kGfJJLY9Nlc= -github.com/matttproud/golang_protobuf_extensions v1.0.1/go.mod h1:D8He9yQNgCq6Z5Ld7szi9bcBfOoFv/3dc6xSMkL2PC0= -github.com/matttproud/golang_protobuf_extensions v1.0.2-0.20181231171920-c182affec369 h1:I0XW9+e1XWDxdcEniV4rQAIOPUGDq67JSCiRCgGCZLI= -github.com/matttproud/golang_protobuf_extensions v1.0.2-0.20181231171920-c182affec369/go.mod h1:BSXmuO+STAnVfrANrmjBb36TMTDstsz7MSK+HVaYKv4= github.com/mitchellh/mapstructure v1.5.0 h1:jeMsZIYE/09sWLaz43PL7Gy6RuMjD2eJVyuac5Z2hdY= github.com/mitchellh/mapstructure v1.5.0/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo= -github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= -github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg= -github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= -github.com/modern-go/reflect2 v0.0.0-20180701023420-4b7aa43c6742/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0= -github.com/modern-go/reflect2 v1.0.1/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0= -github.com/modern-go/reflect2 v1.0.2 h1:xBagoLtFs94CBntxluKeaWgTMpvLxC4ur3nMaC9Gz0M= -github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk= github.com/montanaflynn/stats v0.0.0-20171201202039-1bf9dbcd8cbe h1:iruDEfMl2E6fbMZ9s0scYfZQ84/6SPL6zC8ACM2oIL0= github.com/montanaflynn/stats v0.0.0-20171201202039-1bf9dbcd8cbe/go.mod h1:wL8QJuTMNUDYhXwkmfOly8iTdp5TEcJFWZD2D7SIkUc= -github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 h1:C3w9PqII01/Oq1c1nUAm88MOHcQC9l5mIlSMApZMrHA= -github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822/go.mod h1:+n7T8mK8HuQTcFwEeznm/DIxMOiR9yIdICNftLE1DvQ= -github.com/mwitkow/go-conntrack v0.0.0-20161129095857-cc309e4a2223/go.mod h1:qRWi+5nqEBWmkhHvq77mSJWrCKwh8bxhgT7d/eI7P4U= -github.com/mwitkow/go-conntrack v0.0.0-20190716064945-2f068394615f/go.mod h1:qRWi+5nqEBWmkhHvq77mSJWrCKwh8bxhgT7d/eI7P4U= -github.com/nxadm/tail v1.4.8 h1:nPr65rt6Y5JFSKQO7qToXr7pePgD6Gwiw05lkbyAQTE= -github.com/oklog/ulid v1.3.1 h1:EGfNDEx6MqHz8B3uNV6QAib1UR2Lm97sHi3ocA6ESJ4= -github.com/oklog/ulid v1.3.1/go.mod h1:CirwcVhetQ6Lv90oh/F+FBtV6XMibvdAFo93nm5qn4U= -github.com/onsi/ginkgo v1.16.5 h1:8xi0RTUf59SOSfEtZMvwTvXYMzG4gV23XVHOZiXNtnE= -github.com/onsi/ginkgo/v2 v2.9.1 h1:zie5Ly042PD3bsCvsSOPvRnFwyo3rKe64TJlD6nu0mk= -github.com/onsi/gomega v1.27.4 h1:Z2AnStgsdSayCMDiCU42qIz+HLqEPcgiOCXjAU/w+8E= github.com/pelletier/go-toml v1.9.5 h1:4yBQzkHv+7BHq2PQUZF3Mx0IYxG7LsP222s7Agd3ve8= github.com/pelletier/go-toml v1.9.5/go.mod h1:u1nR/EPcESfeI/szUZKdtJ0xRNbUoANCkoOuaOx1Y+c= github.com/pelletier/go-toml/v2 v2.0.5 h1:ipoSadvV8oGUjnUbMub59IDPPwfxF694nG/jwbMiyQg= github.com/pelletier/go-toml/v2 v2.0.5/go.mod h1:OMHamSCAODeSsVrwwvcJOaoN0LIUIaFVNZzmWyNfXas= -github.com/pierrec/lz4 v2.6.1+incompatible h1:9UY3+iC23yxF0UfGaYrGplQ+79Rg+h/q9FV9ix19jjM= -github.com/pierrec/lz4 v2.6.1+incompatible/go.mod h1:pdkljMzZIN41W+lC3N2tnIh5sFi+IEE17M5jbnwPHcY= -github.com/pkg/errors v0.8.0/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= -github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= +github.com/pjbgf/sha1cd v0.3.2 h1:a9wb0bp1oC2TGwStyn0Umc/IGKQnEgF0vVaZ8QF8eo4= +github.com/pjbgf/sha1cd v0.3.2/go.mod h1:zQWigSxVmsHEZow5qaLtPYxpcKMMQpa09ixqBxuCS6A= github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= github.com/pkg/sftp v1.13.1/go.mod h1:3HaPG6Dq1ILlpPZRO0HVMrsydcdLt6HRDccSgb87qRg= github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= -github.com/prometheus/client_golang v0.9.1/go.mod h1:7SWBe2y4D6OKWSNQJUaRYU/AaXPKyh/dDVn+NZz0KFw= -github.com/prometheus/client_golang v1.0.0/go.mod h1:db9x61etRT2tGnBNRi70OPL5FsnadC4Ky3P0J6CfImo= -github.com/prometheus/client_golang v1.7.1/go.mod h1:PY5Wy2awLA44sXw4AOSfFBetzPP4j5+D6mVACh+pe2M= -github.com/prometheus/client_golang v1.11.0/go.mod h1:Z6t4BnS23TR94PD6BsDNk8yVqroYurpAkEiz0P2BEV0= -github.com/prometheus/client_golang v1.12.1/go.mod h1:3Z9XVyYiZYEO+YQWt3RD2R3jrbd179Rt297l4aS6nDY= -github.com/prometheus/client_golang v1.13.0 h1:b71QUfeo5M8gq2+evJdTPfZhYMAU0uKPkyPJ7TPsloU= -github.com/prometheus/client_golang v1.13.0/go.mod h1:vTeo+zgvILHsnnj/39Ou/1fPN5nJFOEMgftOUOmlvYQ= -github.com/prometheus/client_model v0.0.0-20180712105110-5c3871d89910/go.mod h1:MbSGuTsp3dbXC40dX6PRTWyKYBIrTGTE9sqQNg2J8bo= -github.com/prometheus/client_model v0.0.0-20190129233127-fd36f4220a90/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA= github.com/prometheus/client_model v0.0.0-20190812154241-14fe0d1b01d4/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA= -github.com/prometheus/client_model v0.2.0 h1:uq5h0d+GuxiXLJLNABMgp2qUWDPiLvgCzz2dUR+/W/M= -github.com/prometheus/client_model v0.2.0/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA= -github.com/prometheus/common v0.4.1/go.mod h1:TNfzLD0ON7rHzMJeJkieUDPYmFC7Snx/y86RQel1bk4= -github.com/prometheus/common v0.10.0/go.mod h1:Tlit/dnDKsSWFlCLTWaA1cyBgKHSMdTB80sz/V91rCo= -github.com/prometheus/common v0.26.0/go.mod h1:M7rCNAaPfAosfx8veZJCuw84e35h3Cfd9VFqTh1DIvc= -github.com/prometheus/common v0.32.1/go.mod h1:vu+V0TpY+O6vW9J44gczi3Ap/oXXR10b+M/gUGO4Hls= -github.com/prometheus/common v0.37.0 h1:ccBbHCgIiT9uSoFY0vX8H3zsNR5eLt17/RQLUvn8pXE= -github.com/prometheus/common v0.37.0/go.mod h1:phzohg0JFMnBEFGxTDbfu3QyL5GI8gTQJFhYO5B3mfA= -github.com/prometheus/procfs v0.0.0-20181005140218-185b4288413d/go.mod h1:c3At6R/oaqEKCNdg8wHV1ftS6bRYblBhIjjI8uT2IGk= -github.com/prometheus/procfs v0.0.2/go.mod h1:TjEm7ze935MbeOT/UhFTIMYKhuLP4wbCsTZCD3I8kEA= -github.com/prometheus/procfs v0.1.3/go.mod h1:lV6e/gmhEcM9IjHGsFOCxxuZ+z1YqCvr4OA4YeYWdaU= -github.com/prometheus/procfs v0.6.0/go.mod h1:cz+aTbrPOrUb4q7XlbU9ygM+/jj0fzG6c1xBZuNvfVA= -github.com/prometheus/procfs v0.7.3/go.mod h1:cz+aTbrPOrUb4q7XlbU9ygM+/jj0fzG6c1xBZuNvfVA= -github.com/prometheus/procfs v0.8.0 h1:ODq8ZFEaYeCaZOJlZZdJA2AbQR98dSHSM1KW/You5mo= -github.com/prometheus/procfs v0.8.0/go.mod h1:z7EfXMXOkbkqb9IINtpCn86r/to3BnA0uaxHdg830/4= -github.com/rcrowley/go-metrics v0.0.0-20201227073835-cf1acfcdf475 h1:N/ElC8H3+5XpJzTSTfLsJV/mx9Q9g7kxmchpfZyxgzM= -github.com/rcrowley/go-metrics v0.0.0-20201227073835-cf1acfcdf475/go.mod h1:bCqnVzQkZxMG4s8nGwiZ5l3QUCyqpo9Y+/ZMZ9VjZe4= -github.com/rogpeppe/fastuuid v1.2.0/go.mod h1:jVj6XXZzXRy/MSR5jhDC/2q6DgLz+nrA6LYCDYWNEvQ= +github.com/redis/go-redis/v9 v9.18.0 h1:pMkxYPkEbMPwRdenAzUNyFNrDgHx9U+DrBabWNfSRQs= +github.com/redis/go-redis/v9 v9.18.0/go.mod h1:k3ufPphLU5YXwNTUcCRXGxUoF1fqxnhFQmscfkCoDA0= github.com/rogpeppe/go-internal v1.3.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4= -github.com/rogpeppe/go-internal v1.6.1/go.mod h1:xXDCJY+GAPziupqXw64V24skbSoqbTEfhy4qGm1nDQc= -github.com/rogpeppe/go-internal v1.10.0 h1:TMyTOH3F/DB16zRVcYyreMH6GnZZrwQVAoYjRBZyWFQ= -github.com/russross/blackfriday/v2 v2.0.1/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= -github.com/shurcooL/sanitized_anchor_name v1.0.0/go.mod h1:1NzhyTcUVG4SuEtjjoZeVRXNmyL/1OwPU0+IJeTBvfc= -github.com/sirupsen/logrus v1.2.0/go.mod h1:LxeOpSwHxABJmUn/MG1IvRgCAasNZTLOkJPxbbu5VWo= -github.com/sirupsen/logrus v1.4.2/go.mod h1:tLMulIdttU9McNUspp0xgXVQah82FyeX6MwdIuYE2rE= -github.com/sirupsen/logrus v1.6.0/go.mod h1:7uNnSEd1DgxDLC74fIahvMZmmYsHGZGEOFrfsX/uA88= -github.com/sirupsen/logrus v1.8.1/go.mod h1:yWOB1SBYBC5VeMP7gHvWumXLIWorT60ONWic61uBYv0= -github.com/spaolacci/murmur3 v0.0.0-20180118202830-f09979ecbc72/go.mod h1:JwIasOWyU6f++ZhiEuf87xNszmSA2myDM2Kzu9HwQUA= +github.com/rogpeppe/go-internal v1.13.1 h1:KvO1DLK/DRN07sQ1LQKScxyZJuNnedQ5/wKSR38lUII= +github.com/rogpeppe/go-internal v1.13.1/go.mod h1:uMEvuHeurkdAXX61udpOXGD/AzZDWNMNyH2VO9fmH0o= +github.com/sergi/go-diff v1.3.2-0.20230802210424-5b0b94c5c0d3 h1:n661drycOFuPLCN3Uc8sB6B/s6Z4t2xvBgU1htSHuq8= +github.com/sergi/go-diff v1.3.2-0.20230802210424-5b0b94c5c0d3/go.mod h1:A0bzQcvG0E7Rwjx0REVgAGH58e96+X0MeOfepqsbeW4= +github.com/skeema/knownhosts v1.3.1 h1:X2osQ+RAjK76shCbvhHHHVl3ZlgDm8apHEHFqRjnBY8= +github.com/skeema/knownhosts v1.3.1/go.mod h1:r7KTdC8l4uxWRyK2TpQZ/1o5HaSzh06ePQNxPwTcfiY= github.com/spf13/afero v1.9.2 h1:j49Hj62F0n+DaZ1dDCvhABaPNSGNkt32oRFxI33IEMw= github.com/spf13/afero v1.9.2/go.mod h1:iUV7ddyEEZPO5gA3zD4fJt6iStLlL+Lg4m2cihcDf8Y= github.com/spf13/cast v1.5.0 h1:rj3WzYc11XZaIZMPKmwP96zkFEnnAmV8s6XbB2aY32w= @@ -373,43 +237,39 @@ github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA= github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= github.com/spf13/viper v1.13.0 h1:BWSJ/M+f+3nmdz9bxB+bWX28kkALN2ok11D0rSo8EJU= github.com/spf13/viper v1.13.0/go.mod h1:Icm2xNL3/8uyh/wFuB1jI7TiTNKp8632Nwegu+zgdYw= -github.com/stoewer/go-strcase v1.2.0/go.mod h1:IBiWB2sKIp3wVVQ3Y035++gc+knqhUQag1KpM8ahLw8= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= -github.com/stretchr/objx v0.1.1/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw= -github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo= github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= -github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4= github.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5cxcmMvtA= github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= -github.com/stretchr/testify v1.8.1 h1:w7B6lhMri9wdJUVmEZPGGhZzrYTPvgJArz7wNPgYKsk= -github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4= +github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U= +github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U= github.com/subosito/gotenv v1.4.1 h1:jyEFiXpy21Wm81FBN71l9VoMMV8H8jG+qIK3GCpY6Qs= github.com/subosito/gotenv v1.4.1/go.mod h1:ayKnFf/c6rvx/2iiLrJUk1e6plDbT3edrFNGqEflhK0= github.com/tidwall/pretty v1.0.0 h1:HsD+QiTn7sK6flMKIvNmpqz1qrpP3Ps6jOKIKMooyg4= github.com/tidwall/pretty v1.0.0/go.mod h1:XNkn88O1ChpSDQmQeStsy+sBenx6DDtFZJxhVysOjyk= -github.com/urfave/cli/v2 v2.3.0/go.mod h1:LJmUH05zAU44vOAcrfzZQKsZbVcdbOG8rtL3/XcUArI= +github.com/xanzy/ssh-agent v0.3.3 h1:+/15pJfg/RsTxqYcX6fHqOXZwwMP+2VyYWJeWM2qQFM= +github.com/xanzy/ssh-agent v0.3.3/go.mod h1:6dzNDKs0J9rVPHPhaGCukekBHKqfl+L3KghI1Bc68Uw= github.com/xdg-go/pbkdf2 v1.0.0 h1:Su7DPu48wXMwC3bs7MCNG+z4FhcyEuz5dlvchbq0B0c= github.com/xdg-go/pbkdf2 v1.0.0/go.mod h1:jrpuAogTd400dnrH08LKmI/xc1MbPOebTwRqcT5RDeI= -github.com/xdg-go/scram v1.1.0/go.mod h1:1WAq6h33pAW+iRreB34OORO2Nf7qel3VV3fjBj+hCSs= github.com/xdg-go/scram v1.1.1 h1:VOMT+81stJgXW3CpHyqHN3AXDYIMsx56mEFrB37Mb/E= github.com/xdg-go/scram v1.1.1/go.mod h1:RaEWvsqvNKKvBPvcKeFjrG2cJqOkHTiyTpzz23ni57g= -github.com/xdg-go/stringprep v1.0.2/go.mod h1:8F9zXuvzgwmyT5DUm4GUfZGDdT3W+LCvS6+da4O5kxM= github.com/xdg-go/stringprep v1.0.3 h1:kdwGpVNwPFtjs98xCGkHjQtGKh86rDcRZN17QEMCOIs= github.com/xdg-go/stringprep v1.0.3/go.mod h1:W3f5j4i+9rC0kuIEJL0ky1VpHXQU3ocBgklLGvcBnW8= -github.com/xeipuuv/gojsonpointer v0.0.0-20180127040702-4e3ac2762d5f/go.mod h1:N2zxlSyiKSe5eX1tZViRH5QA0qijqEDrYZiPEAiq3wU= -github.com/xeipuuv/gojsonreference v0.0.0-20180127040603-bd5ef7bd5415/go.mod h1:GwrjFmJcFw6At/Gs6z4yjiIwzuJ1/+UwLxMQDVQXShQ= -github.com/xeipuuv/gojsonschema v1.2.0/go.mod h1:anYRn/JVcOK2ZgGU+IjEV4nwlhoK5sQluxsYJ78Id3Y= github.com/youmark/pkcs8 v0.0.0-20181117223130-1be2e3e5546d h1:splanxYIlg+5LfHAM6xpdFEAYOk8iySO56hMFq6uLyA= github.com/youmark/pkcs8 v0.0.0-20181117223130-1be2e3e5546d/go.mod h1:rHwXgn7JulP+udvsHwJoVG1YGAP6VLg4y9I5dyZdqmA= github.com/yuin/goldmark v1.1.25/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= github.com/yuin/goldmark v1.1.32/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= +github.com/zclconf/go-cty v1.17.0 h1:seZvECve6XX4tmnvRzWtJNHdscMtYEx5R7bnnVyd/d0= +github.com/zclconf/go-cty v1.17.0/go.mod h1:wqFzcImaLTI6A5HfsRwB0nj5n0MRZFwmey8YoFPPs3U= +github.com/zeebo/xxh3 v1.0.2 h1:xZmwmqxHZA8AI603jOQ0tMqmBr9lPeFwGg6d+xy9DC0= +github.com/zeebo/xxh3 v1.0.2/go.mod h1:5NWz9Sef7zIDm2JHfFlcQvNekmcEl9ekUZQQKCYaDcA= go.mongodb.org/mongo-driver v1.11.1 h1:QP0znIRTuL0jf1oBQoAoM0C6ZJfBK4kx0Uumtv1A7w8= go.mongodb.org/mongo-driver v1.11.1/go.mod h1:s7p5vEtfbeR1gYi6pnj3c3/urpbLv2T5Sfd6Rp2HBB8= go.opencensus.io v0.21.0/go.mod h1:mSImk1erAIZhrmZN+AvHh14ztQfjbGwt4TtuofqLduU= @@ -418,30 +278,32 @@ go.opencensus.io v0.22.2/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw= go.opencensus.io v0.22.3/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw= go.opencensus.io v0.22.4/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw= go.opencensus.io v0.22.5/go.mod h1:5pWMHQbX5EPX2/62yrJeAkowc+lfs/XD7Uxpq3pI6kk= -go.opentelemetry.io/contrib/instrumentation/github.com/Shopify/sarama/otelsarama v0.31.0 h1:J8jI81RCB7U9a3qsTZXM/38XrvbLJCye6J32bfQctYY= -go.opentelemetry.io/contrib/instrumentation/github.com/Shopify/sarama/otelsarama v0.31.0/go.mod h1:72+cPzsW6geApbceSLMbZtYZeGMgtRDw5TcSEsdGlhc= -go.opentelemetry.io/otel v1.6.1 h1:6r1YrcTenBvYa1x491d0GGpTVBsNECmrc/K6b+zDeis= -go.opentelemetry.io/otel v1.6.1/go.mod h1:blzUabWHkX6LJewxvadmzafgh/wnvBSDBdOuwkAtrWQ= -go.opentelemetry.io/otel/trace v1.6.1 h1:f8c93l5tboBYZna1nWk0W9DYyMzJXDWdZcJZ0Kb400U= -go.opentelemetry.io/otel/trace v1.6.1/go.mod h1:RkFRM1m0puWIq10oxImnGEduNBzxiN7TXluRBtE+5j0= -go.opentelemetry.io/proto/otlp v0.7.0/go.mod h1:PqfVotwruBrMGOCsRd/89rSnXhoiJIqeYNgFYFoEGnI= -go.uber.org/atomic v1.10.0 h1:9qC72Qh0+3MqyJbAn8YU5xVq1frD8bn3JtD2oXtafVQ= -go.uber.org/goleak v1.1.12 h1:gZAh5/EyT/HQwlpkCy6wTpqfH9H8Lz8zbm3dZh+OyzA= -go.uber.org/multierr v1.8.0 h1:dg6GjLku4EH+249NNmoIciG9N/jURbDG+pFlTkhzIC8= -go.uber.org/zap v1.22.0 h1:Zcye5DUgBloQ9BaT4qc9BnjOFog5TvBSAGkJ3Nf70c0= -golang.org/x/crypto v0.0.0-20180904163835-0709b304e793/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4= +go.opentelemetry.io/otel v1.32.0 h1:WnBN+Xjcteh0zdk01SVqV55d/m62NJLJdIyb4y/WO5U= +go.opentelemetry.io/otel v1.32.0/go.mod h1:00DCVSB0RQcnzlwyTfqtxSm+DRr9hpYrHjNGiBHVQIg= +go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.32.0 h1:IJFEoHiytixx8cMiVAO+GmHR6Frwu+u5Ur8njpFO6Ac= +go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.32.0/go.mod h1:3rHrKNtLIoS0oZwkY2vxi+oJcwFRWdtUyRII+so45p8= +go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.32.0 h1:cMyu9O88joYEaI47CnQkxO1XZdpoTF9fEnW2duIddhw= +go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.32.0/go.mod h1:6Am3rn7P9TVVeXYG+wtcGE7IE1tsQ+bP3AuWcKt/gOI= +go.opentelemetry.io/otel/metric v1.32.0 h1:xV2umtmNcThh2/a/aCP+h64Xx5wsj8qqnkYZktzNa0M= +go.opentelemetry.io/otel/metric v1.32.0/go.mod h1:jH7CIbbK6SH2V2wE16W05BHCtIDzauciCRLoc/SyMv8= +go.opentelemetry.io/otel/sdk v1.32.0 h1:RNxepc9vK59A8XsgZQouW8ue8Gkb4jpWtJm9ge5lEG4= +go.opentelemetry.io/otel/sdk v1.32.0/go.mod h1:LqgegDBjKMmb2GC6/PrTnteJG39I8/vJCAP9LlJXEjU= +go.opentelemetry.io/otel/trace v1.32.0 h1:WIC9mYrXf8TmY/EXuULKc8hR17vE+Hjv2cssQDe03fM= +go.opentelemetry.io/otel/trace v1.32.0/go.mod h1:+i4rkvCraA+tG6AzwloGaCtkx53Fa+L+V8e9a7YvhT8= +go.opentelemetry.io/proto/otlp v1.3.1 h1:TrMUixzpM0yuc/znrFTP9MMRh8trP93mkCiDVeXrui0= +go.opentelemetry.io/proto/otlp v1.3.1/go.mod h1:0X1WI4de4ZsLrrJNLAQbFeLCm3T7yBkR0XqQ7niQU+8= +go.uber.org/atomic v1.11.0 h1:ZvwS0R+56ePWxUNi+Atn9dWONBPp/AUETXlHW0DxSjE= +go.uber.org/atomic v1.11.0/go.mod h1:LUxbIzbOniOlMKjJjyPfpl4v+PKK2cNJn91OQbhoJI0= golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= golang.org/x/crypto v0.0.0-20190510104115-cbcb75029529/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= golang.org/x/crypto v0.0.0-20190605123033-f99c8df09eb5/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= -golang.org/x/crypto v0.0.0-20201112155050-0c6587e931a9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= golang.org/x/crypto v0.0.0-20210421170649-83a5a9bb288b/go.mod h1:T9bdIzuCu7OtxOm1hfPfRQxPLYneinmdGuTeoZ9dtd4= golang.org/x/crypto v0.0.0-20211108221036-ceb1ce70b4fa/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= -golang.org/x/crypto v0.0.0-20220214200702-86341886e292/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4= golang.org/x/crypto v0.0.0-20220622213112-05595931fe9d/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4= -golang.org/x/crypto v0.6.0 h1:qfktjS5LUO+fFKeJXZ+ikTRijMmljikvG68fpMMruSc= -golang.org/x/crypto v0.6.0/go.mod h1:OFC/31mSvZgRz0V1QTNCzfAI1aIRzbiufJtkMIlEp58= +golang.org/x/crypto v0.45.0 h1:jMBrvKuj23MTlT0bQEOBcAE0mjg8mK9RXFhRH6nyF3Q= +golang.org/x/crypto v0.45.0/go.mod h1:XTGrrkGJve7CYK7J8PEww4aY7gM3qMCElcJQ8n8JdX4= golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= golang.org/x/exp v0.0.0-20190306152737-a1d7652674e8/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= golang.org/x/exp v0.0.0-20190510132918-efd6b22b2522/go.mod h1:ZjyILWgesfNpC6sMxTJOJm9Kp84zZh5NQWvqDGG3Qr8= @@ -475,9 +337,10 @@ golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= golang.org/x/mod v0.4.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= golang.org/x/mod v0.4.1/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= +golang.org/x/mod v0.33.0 h1:tHFzIWbBifEmbwtGz65eaWyGiGZatSrT9prnU8DbVL8= +golang.org/x/mod v0.33.0/go.mod h1:swjeQEj+6r7fODbD2cqrnje9PnziFuw4bmLbBZFrQ5w= golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= -golang.org/x/net v0.0.0-20181114220301-adae6a3d119a/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20190108225652-1e06a53dbb7e/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20190213061140-3a22650c66bd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= @@ -485,7 +348,6 @@ golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn golang.org/x/net v0.0.0-20190501004415-9ce7a6920f09/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= golang.org/x/net v0.0.0-20190503192946-f4e77d36d62c/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= golang.org/x/net v0.0.0-20190603091049-60506f45cf65/go.mod h1:HSz+uSET+XFnRR8LxR5pz3Of3rY3CfYBVs4xY44aLks= -golang.org/x/net v0.0.0-20190613194153-d28f0bde5980/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20190628185345-da137c7871d7/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20190724013045-ca1201d0de80/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= @@ -508,14 +370,9 @@ golang.org/x/net v0.0.0-20201031054903-ff519b6c9102/go.mod h1:sp8m0HH+o8qH0wwXwY golang.org/x/net v0.0.0-20201209123823-ac852fbbde11/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= golang.org/x/net v0.0.0-20201224014010-6772e930b67b/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= -golang.org/x/net v0.0.0-20210405180319-a5a99cb37ef4/go.mod h1:p54w0d4576C0XHj96bSt6lcn1PtDYWL6XObtHCRCNQM= -golang.org/x/net v0.0.0-20210525063256-abc453219eb5/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= -golang.org/x/net v0.0.0-20210805182204-aaa1db679c0d/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= golang.org/x/net v0.0.0-20211112202133-69e39bad7dc2/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= -golang.org/x/net v0.0.0-20220127200216-cd36cc0744dd/go.mod h1:CfG3xpIq0wQ8r1q4Su4UZFWDARRcnwPjda9FqA0JpMk= -golang.org/x/net v0.0.0-20220225172249-27dd8689420f/go.mod h1:CfG3xpIq0wQ8r1q4Su4UZFWDARRcnwPjda9FqA0JpMk= -golang.org/x/net v0.8.0 h1:Zrh2ngAOFYneWTAIAPethzeaQLuHwhuBkuV6ZiRnUaQ= -golang.org/x/net v0.8.0/go.mod h1:QVkue5JL9kW//ek3r6jTKnTFis1tRmNAW2P1shuFdJc= +golang.org/x/net v0.47.0 h1:Mx+4dIFzqraBXUugkia1OOvlD6LemFo1ALMHjrXDOhY= +golang.org/x/net v0.47.0/go.mod h1:/jNxtkgq5yWUGYkaZGqo27cfGZ1c5Nen03aYrrKpVRU= golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= golang.org/x/oauth2 v0.0.0-20190226205417-e64efc72b421/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= golang.org/x/oauth2 v0.0.0-20190604053449-0f29369cfe45/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= @@ -525,10 +382,6 @@ golang.org/x/oauth2 v0.0.0-20200902213428-5d25da1a8d43/go.mod h1:KelEdhl1UZF7XfJ golang.org/x/oauth2 v0.0.0-20201109201403-9fd604954f58/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A= golang.org/x/oauth2 v0.0.0-20201208152858-08078c50e5b5/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A= golang.org/x/oauth2 v0.0.0-20210218202405-ba52d332ba99/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A= -golang.org/x/oauth2 v0.0.0-20210514164344-f6687ab2804c/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A= -golang.org/x/oauth2 v0.0.0-20220223155221-ee480838109b/go.mod h1:DAh4E804XQdzx2j+YRIaUnCqCV2RuMz24cGBJ5QYIrc= -golang.org/x/oauth2 v0.0.0-20221014153046-6fdb5e3db783 h1:nt+Q6cXKz4MosCSpnbMtqiQ8Oz0pxTef2B4Vca2lvfk= -golang.org/x/oauth2 v0.0.0-20221014153046-6fdb5e3db783/go.mod h1:h4gKUeWbJ4rQPri7E0u6Gs4e9Ri2zaLxzw5DI5XGrYg= golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= @@ -540,25 +393,20 @@ golang.org/x/sync v0.0.0-20200625203802-6e8e738ad208/go.mod h1:RxMgew5VJxzue5/jJ golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20201207232520-09787c993a3a/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= -golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4 h1:uVc8UZUe6tr40fFVnUP5Oj+veunVezqYl9z7DYw9xzw= -golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.18.0 h1:kr88TuHDroi+UVf+0hZnirlk8o8T+4MrK6mr60WkH/I= +golang.org/x/sync v0.18.0/go.mod h1:9KTHXmSnoGruLpwFjVSX0lNNA75CykiMECbovNTZqGI= golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= -golang.org/x/sys v0.0.0-20180905080454-ebe1bf3edb33/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= -golang.org/x/sys v0.0.0-20181116152217-5ac8a444bdc5/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20190312061237-fead79001313/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20190422165155-953cdadca894/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20190502145724-3ef323f4f1fd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20190507160741-ecd444e8653b/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20190606165138-5da285871e9c/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20190624142023-c5567b49c5d0/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20190726091711-fc99dfbffb4e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20191001151750-bb3f8db39f24/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20191026070338-33540a1f6037/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20191204072324-ce4227a45e2e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20191228213918-04cbcbbfeed8/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20200106162015-b016eb3dc98e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200113162924-86b910548bc1/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200122134326-e047566fdf82/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200202164722-d101bd2416d5/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= @@ -571,8 +419,6 @@ golang.org/x/sys v0.0.0-20200501052902-10377860bb8e/go.mod h1:h1NjWce9XRLGQEsW7w golang.org/x/sys v0.0.0-20200511232937-7e40ca221e25/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200515095857-1151b9dac4a9/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200523222454-059865788121/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20200615200032-f1bc736245b1/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20200625212154-ddb9806d33ae/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200803210538-64077c9b5642/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200905004654-be1d3432aa8f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= @@ -580,40 +426,27 @@ golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7w golang.org/x/sys v0.0.0-20201201145000-ef89a241ccb3/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210104204734-6f8348627aad/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210119212857-b64e53b001e4/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20210124154548-22da62e12c0c/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210225134936-a50acf3fe073/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20210330210617-4fbd30eecc44/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210423185535-09eb48e85fd7/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20210510120138-977fb7262007/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.0.0-20210603081109-ebe580a85c40/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.0.0-20211216021012-1d35b9e2eb4e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.0.0-20220114195835-da31bd327af9/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220908164124-27713097b956/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.6.0 h1:MVltZSvRTcU2ljQOhs94SXPftV6DCNnZViHeQps87pQ= -golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.38.0 h1:3yZWxaJjBmCWXqhN1qh02AkOnCQ1poK6oF+a7xWL6Gc= +golang.org/x/sys v0.38.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks= golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= -golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= -golang.org/x/term v0.6.0 h1:clScbb1cHjoCkyRbWwBEUZ5H/tIFu5TAXIqaZD0Gcjw= -golang.org/x/term v0.6.0/go.mod h1:m6U89DPEgQRMq3DNkDClhWw02AUbt2daBVO4cn4Hv9U= golang.org/x/text v0.0.0-20170915032832-14c0d48ead0c/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.1-0.20180807135948-17ff2d5776d2/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk= golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.3.4/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= -golang.org/x/text v0.3.5/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= -golang.org/x/text v0.8.0 h1:57P1ETyNKtuIjB4SRd15iJxuhj8Gc416Y78H3qgMh68= -golang.org/x/text v0.8.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8= +golang.org/x/text v0.31.0 h1:aC8ghyu4JhP8VojJ2lEHBnochRno1sgL6nEi9WGFGMM= +golang.org/x/text v0.31.0/go.mod h1:tKRAlv61yKIjGGHX/4tP1LTbc13YSec1pxVEWXzfoeM= golang.org/x/time v0.0.0-20181108054448-85acf8d2951c/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= golang.org/x/time v0.0.0-20190308202827-9d24e82272b4/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= golang.org/x/time v0.0.0-20191024005414-555d28b269f0/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= -golang.org/x/time v0.0.0-20220722155302-e5dcc9cfc0b9 h1:ftMN5LMiBFjbzleLqtoBZk7KdJwhuybIU+FckUHgoyQ= -golang.org/x/time v0.0.0-20220722155302-e5dcc9cfc0b9/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= -golang.org/x/tools v0.0.0-20180221164845-07fd8470d635/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= golang.org/x/tools v0.0.0-20190114222345-bf090417da8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= golang.org/x/tools v0.0.0-20190226205152-f727befe758c/go.mod h1:9Yl7xja0Znq3iFh3HoIrodX9oNMXvdceNzlUR8zjMvY= @@ -651,7 +484,6 @@ golang.org/x/tools v0.0.0-20200501065659-ab2804fb9c9d/go.mod h1:EkVYQZoAsY45+roY golang.org/x/tools v0.0.0-20200512131952-2bc93b1c0c88/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= golang.org/x/tools v0.0.0-20200515010526-7d3b6ebf133d/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= golang.org/x/tools v0.0.0-20200618134242-20370b0cb4b2/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= -golang.org/x/tools v0.0.0-20200619180055-7c47624df98f/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= golang.org/x/tools v0.0.0-20200729194436-6467de6f59a7/go.mod h1:njjCfa9FT2d7l9Bc6FUM5FLjQPp3cFF28FI3qnDFljA= golang.org/x/tools v0.0.0-20200804011535-6c149bb5ef0d/go.mod h1:njjCfa9FT2d7l9Bc6FUM5FLjQPp3cFF28FI3qnDFljA= golang.org/x/tools v0.0.0-20200825202427-b303f430e36d/go.mod h1:njjCfa9FT2d7l9Bc6FUM5FLjQPp3cFF28FI3qnDFljA= @@ -660,16 +492,12 @@ golang.org/x/tools v0.0.0-20201110124207-079ba7bd75cd/go.mod h1:emZCQorbCU4vsT4f golang.org/x/tools v0.0.0-20201201161351-ac6f37ff4c2a/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= golang.org/x/tools v0.0.0-20201208233053-a543418bbed2/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= golang.org/x/tools v0.0.0-20210105154028-b0ab187a4818/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= -golang.org/x/tools v0.0.0-20210106214847-113979e3529a/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= golang.org/x/tools v0.0.0-20210108195828-e2f9c7f1fc8e/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= golang.org/x/tools v0.1.0/go.mod h1:xkSsbof2nBLbhDlRMhhhyNLN/zl3eTqcnHD5viDpcZ0= -golang.org/x/tools v0.7.0 h1:W4OVu8VVOaIO0yzWMNdepAulS7YfoS3Zabrm8DOXXU4= golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= -gomodules.xyz/jsonpatch/v2 v2.2.0 h1:4pT439QV83L+G9FkcCriY6EkpcK6r6bK+A5FBUMI7qY= -gomodules.xyz/jsonpatch/v2 v2.2.0/go.mod h1:WXp+iVDkoLQqPudfQ9GBlwB2eZ5DKOnjQZCYdOS8GPY= google.golang.org/api v0.4.0/go.mod h1:8k5glujaEP+g9n7WNsDg8QP6cUVNI86fCNMcbazEtwE= google.golang.org/api v0.7.0/go.mod h1:WtwebWUNSVBH/HAw79HIFXZNqEvBhG+Ra+ax0hx3E3M= google.golang.org/api v0.8.0/go.mod h1:o4eAsZoiT+ibD93RtjEohWalFOjRDx6CVaqeizhEnKg= @@ -695,7 +523,6 @@ google.golang.org/appengine v1.5.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7 google.golang.org/appengine v1.6.1/go.mod h1:i06prIuMbXzDqacNJfV5OdTW448YApPu5ww/cMBSeb0= google.golang.org/appengine v1.6.5/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc= google.golang.org/appengine v1.6.6/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc= -google.golang.org/appengine v1.6.7 h1:FZR1q0exgwxzPzp/aF+VccGrSfxfPpkBqjIIEq3ru6c= google.golang.org/appengine v1.6.7/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc= google.golang.org/genproto v0.0.0-20180817151627-c66870c02cf8/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc= google.golang.org/genproto v0.0.0-20190307195333-5fe7a883aa19/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE= @@ -720,7 +547,6 @@ google.golang.org/genproto v0.0.0-20200312145019-da6875a35672/go.mod h1:55QSHmfG google.golang.org/genproto v0.0.0-20200331122359-1ee6d9798940/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= google.golang.org/genproto v0.0.0-20200430143042-b979b6f78d84/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= google.golang.org/genproto v0.0.0-20200511104702-f5ebc3bea380/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= -google.golang.org/genproto v0.0.0-20200513103714-09dca8ec2884/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= google.golang.org/genproto v0.0.0-20200515170657-fc4c6c6a6587/go.mod h1:YsZOwe1myG/8QRHRsmBRE1LrgQY60beZKjly0O1fX9U= google.golang.org/genproto v0.0.0-20200526211855-cb27e3aa2013/go.mod h1:NbSheEEYHJ7i3ixzK3sjbqSGDJWnxyFXZblF3eUsNvo= google.golang.org/genproto v0.0.0-20200618031413-b414f8b61790/go.mod h1:jDfRM7FcilCzHH/e9qn6dsT145K34l5v+OpcnNgKAAA= @@ -734,7 +560,6 @@ google.golang.org/genproto v0.0.0-20201210142538-e3217bee35cc/go.mod h1:FWY/as6D google.golang.org/genproto v0.0.0-20201214200347-8c77b98c765d/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= google.golang.org/genproto v0.0.0-20210108203827-ffc7fda8c3d7/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= google.golang.org/genproto v0.0.0-20210226172003-ab064af71705/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= -google.golang.org/genproto v0.0.0-20220107163113-42d7afdf6368/go.mod h1:5CzLGKJ67TSI2B9POpiiyGha0AjJvZIUgRMt1dSmuhc= google.golang.org/genproto v0.0.0-20221118155620-16455021b5e6 h1:a2S6M0+660BgMNl++4JPlcAO/CjkqYItDEZwkoDQK7c= google.golang.org/genproto v0.0.0-20221118155620-16455021b5e6/go.mod h1:rZS5c/ZVYMaOGBfO68GWtjOw/eLaZM1X6iVtgjZ+EWg= google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c= @@ -750,14 +575,11 @@ google.golang.org/grpc v1.29.1/go.mod h1:itym6AZVZYACWQqET3MqgPpjcuV5QH3BxFS3Iji google.golang.org/grpc v1.30.0/go.mod h1:N36X2cJ7JwdamYAgDz+s+rVMFjt3numwzf/HckM8pak= google.golang.org/grpc v1.31.0/go.mod h1:N36X2cJ7JwdamYAgDz+s+rVMFjt3numwzf/HckM8pak= google.golang.org/grpc v1.31.1/go.mod h1:N36X2cJ7JwdamYAgDz+s+rVMFjt3numwzf/HckM8pak= -google.golang.org/grpc v1.33.1/go.mod h1:fr5YgcSWrqhRRxogOsw7RzIpsmvOZ6IcH4kBYTpR3n0= google.golang.org/grpc v1.33.2/go.mod h1:JMHMWHQWaTccqQQlmk3MJZS+GWXOdAesneDmEnv2fbc= google.golang.org/grpc v1.34.0/go.mod h1:WotjhfgOW/POjDeRt8vscBtXq+2VjORFy659qA51WJ8= google.golang.org/grpc v1.35.0/go.mod h1:qjiiYl8FncCW8feJPdyg3v6XW24KsRHe+dy9BAGRRjU= -google.golang.org/grpc v1.36.0/go.mod h1:qjiiYl8FncCW8feJPdyg3v6XW24KsRHe+dy9BAGRRjU= -google.golang.org/grpc v1.40.0/go.mod h1:ogyxbiOoUXAkP+4+xa6PZSE9DZgIHtSpzjDTB9KAK34= -google.golang.org/grpc v1.52.0 h1:kd48UiU7EHsV4rnLyOJRuP/Il/UHE7gdDAQ+SZI7nZk= -google.golang.org/grpc v1.52.0/go.mod h1:pu6fVzoFb+NBYNAvQL08ic+lvB2IojljRYuun5vorUY= +google.golang.org/grpc v1.67.1 h1:zWnc1Vrcno+lHZCOofnIMvycFcc0QRGIzm9dhnDX68E= +google.golang.org/grpc v1.67.1/go.mod h1:1gLDyUQU7CTLJI90u3nXZ9ekeghjeM7pTDZlqFNg2AA= google.golang.org/protobuf v0.0.0-20200109180630-ec00e32a8dfd/go.mod h1:DFci5gLYBciE7Vtevhsrf46CRTquxDuWsQurQQe4oz8= google.golang.org/protobuf v0.0.0-20200221191635-4d8936d0db64/go.mod h1:kwYJMbMJ01Woi6D6+Kah6886xMZcty6N08ah7+eCXa0= google.golang.org/protobuf v0.0.0-20200228230310-ab0ca4ff8a60/go.mod h1:cfTl7dwQJ+fmap5saPgwCLgHXTUD7jkjRqWcaiX5VyM= @@ -768,37 +590,21 @@ google.golang.org/protobuf v1.23.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2 google.golang.org/protobuf v1.23.1-0.20200526195155-81db48ad09cc/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU= google.golang.org/protobuf v1.24.0/go.mod h1:r/3tXBNzIEhYS9I1OUVjXDlt8tc493IdKGjtUeSXeh4= google.golang.org/protobuf v1.25.0/go.mod h1:9JNX74DMeImyA3h4bdi1ymwjUzf21/xIlbajtzgsN7c= -google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw= -google.golang.org/protobuf v1.26.0/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc= -google.golang.org/protobuf v1.27.1/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc= -google.golang.org/protobuf v1.28.1 h1:d0NfwRgPtno5B1Wa6L2DAG+KivqkdutMf1UhdNx175w= -google.golang.org/protobuf v1.28.1/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqwMG9pJV4I= -gopkg.in/alecthomas/kingpin.v2 v2.2.6/go.mod h1:FMv+mEhP44yOT+4EoQTLFTRgOQ1FBLkstjWtayDeSgw= +google.golang.org/protobuf v1.35.1 h1:m3LfL6/Ca+fqnjnlqQXNpFPABW1UD7mjh8KO2mKFytA= +google.golang.org/protobuf v1.35.1/go.mod h1:9fA7Ob0pmnwhb644+1+CVWFRbNajQ6iRojtC/QF5bRE= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= -gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk= gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q= gopkg.in/errgo.v2 v2.1.0/go.mod h1:hNsd1EY+bozCKY1Ytp96fpM3vjJbqLJn88ws8XvfDNI= -gopkg.in/inf.v0 v0.9.1 h1:73M5CoZyi3ZLMOyDlQh031Cx6N9NDJ2Vvfl76EDAgDc= -gopkg.in/inf.v0 v0.9.1/go.mod h1:cWUDdTG/fYaXco+Dcufb5Vnc6Gp2YChqWtbxRZE0mXw= gopkg.in/ini.v1 v1.67.0 h1:Dgnx+6+nfE+IfzjUEISNeydPJh9AXNNsWbGP9KzCsOA= gopkg.in/ini.v1 v1.67.0/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k= -gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7 h1:uRGJdciOHaEIrze2W8Q3AKkepLTh2hOroT7a+7czfdQ= -gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7/go.mod h1:dt/ZhP58zS4L8KSrWDmTeBkI65Dw0HsyUHuEVlX15mw= -gopkg.in/yaml.v2 v2.2.1/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gopkg.in/warnings.v0 v0.1.2 h1:wFXVbFY8DY5/xOe1ECiWdKCzZlxgshcYVNkBHstARME= +gopkg.in/warnings.v0 v0.1.2/go.mod h1:jksf8JmL6Qr/oQM2OXTHunEvvTAsrWBLb6OOjuVWRNI= gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= -gopkg.in/yaml.v2 v2.2.3/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= -gopkg.in/yaml.v2 v2.2.4/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= -gopkg.in/yaml.v2 v2.2.5/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= -gopkg.in/yaml.v2 v2.2.8/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= -gopkg.in/yaml.v2 v2.3.0/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY= gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ= gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= -gopkg.in/yaml.v3 v3.0.0-20200615113413-eeeca48fe776/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= -gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= -gopkg.in/yaml.v3 v3.0.0/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= @@ -808,34 +614,6 @@ honnef.co/go/tools v0.0.0-20190523083050-ea95bdfd59fc/go.mod h1:rf3lG4BRIbNafJWh honnef.co/go/tools v0.0.1-2019.2.3/go.mod h1:a3bituU0lyd329TUQxRnasdCoJDkEUEAqEt0JzvZhAg= honnef.co/go/tools v0.0.1-2020.1.3/go.mod h1:X/FiERA/W4tHapMX5mGpAtMSVEeEUOyHaw9vFzvIQ3k= honnef.co/go/tools v0.0.1-2020.1.4/go.mod h1:X/FiERA/W4tHapMX5mGpAtMSVEeEUOyHaw9vFzvIQ3k= -k8s.io/api v0.27.2 h1:+H17AJpUMvl+clT+BPnKf0E3ksMAzoBBg7CntpSuADo= -k8s.io/api v0.27.2/go.mod h1:ENmbocXfBT2ADujUXcBhHV55RIT31IIEvkntP6vZKS4= -k8s.io/apiextensions-apiserver v0.25.0 h1:CJ9zlyXAbq0FIW8CD7HHyozCMBpDSiH7EdrSTCZcZFY= -k8s.io/apiextensions-apiserver v0.25.0/go.mod h1:3pAjZiN4zw7R8aZC5gR0y3/vCkGlAjCazcg1me8iB/E= -k8s.io/apimachinery v0.27.2 h1:vBjGaKKieaIreI+oQwELalVG4d8f3YAMNpWLzDXkxeg= -k8s.io/apimachinery v0.27.2/go.mod h1:XNfZ6xklnMCOGGFNqXG7bUrQCoR04dh/E7FprV6pb+E= -k8s.io/client-go v0.27.2 h1:vDLSeuYvCHKeoQRhCXjxXO45nHVv2Ip4Fe0MfioMrhE= -k8s.io/client-go v0.27.2/go.mod h1:tY0gVmUsHrAmjzHX9zs7eCjxcBsf8IiNe7KQ52biTcQ= -k8s.io/component-base v0.25.0 h1:haVKlLkPCFZhkcqB6WCvpVxftrg6+FK5x1ZuaIDaQ5Y= -k8s.io/component-base v0.25.0/go.mod h1:F2Sumv9CnbBlqrpdf7rKZTmmd2meJq0HizeyY/yAFxk= -k8s.io/klog/v2 v2.100.1 h1:7WCHKK6K8fNhTqfBhISHQ97KrnJNFZMcQvKp7gP/tmg= -k8s.io/klog/v2 v2.100.1/go.mod h1:y1WjHnz7Dj687irZUWR/WLkLc5N1YHtjLdmgWjndZn0= -k8s.io/kube-openapi v0.0.0-20230501164219-8b0f38b5fd1f h1:2kWPakN3i/k81b0gvD5C5FJ2kxm1WrQFanWchyKuqGg= -k8s.io/kube-openapi v0.0.0-20230501164219-8b0f38b5fd1f/go.mod h1:byini6yhqGC14c3ebc/QwanvYwhuMWF6yz2F8uwW8eg= -k8s.io/utils v0.0.0-20230209194617-a36077c30491 h1:r0BAOLElQnnFhE/ApUsg3iHdVYYPBjNSSOMowRZxxsY= -k8s.io/utils v0.0.0-20230209194617-a36077c30491/go.mod h1:OLgZIPagt7ERELqWJFomSt595RzquPNLL48iOWgYOg0= rsc.io/binaryregexp v0.2.0/go.mod h1:qTv7/COck+e2FymRvadv62gMdZztPaShugOCi3I+8D8= rsc.io/quote/v3 v3.1.0/go.mod h1:yEA65RcK8LyAZtP9Kv3t0HmxON59tX3rD+tICJqUlj0= rsc.io/sampler v1.3.0/go.mod h1:T1hPZKmBbMNahiBKFy5HrXp6adAjACjK9JXDnKaTXpA= -sigs.k8s.io/cluster-api v1.3.3 h1:sHRAbuev6+bz3OAySmdmT62md/D/UUIy0EYwvP38H/4= -sigs.k8s.io/cluster-api v1.3.3/go.mod h1:nnXmR51rHshpMEXmB4LZIwdiXWKXV6yaooB1KzrL0Qs= -sigs.k8s.io/cluster-api-provider-vsphere v1.6.1 h1:4BSTJwBF3g4itJIh1nZd4pT5420ccfYs4eG+x2k2OGE= -sigs.k8s.io/cluster-api-provider-vsphere v1.6.1/go.mod h1:abYnQNR3etNBGcnKazIJANzW4BsEDR7pcTmgwbcrbzY= -sigs.k8s.io/controller-runtime v0.13.1 h1:tUsRCSJVM1QQOOeViGeX3GMT3dQF1eePPw6sEE3xSlg= -sigs.k8s.io/controller-runtime v0.13.1/go.mod h1:Zbz+el8Yg31jubvAEyglRZGdLAjplZl+PgtYNI6WNTI= -sigs.k8s.io/json v0.0.0-20221116044647-bc3834ca7abd h1:EDPBXCAspyGV4jQlpZSudPeMmr1bNJefnuqLsRAsHZo= -sigs.k8s.io/json v0.0.0-20221116044647-bc3834ca7abd/go.mod h1:B8JuhiUyNFVKdsE8h686QcCxMaH6HrOAZj4vswFpcB0= -sigs.k8s.io/structured-merge-diff/v4 v4.2.3 h1:PRbqxJClWWYMNV1dhaG4NsibJbArud9kFxnAMREiWFE= -sigs.k8s.io/structured-merge-diff/v4 v4.2.3/go.mod h1:qjx8mGObPmV2aSZepjQjbmb2ihdVs8cGKBraizNC69E= -sigs.k8s.io/yaml v1.3.0 h1:a2VclLzOGrwOHDiV8EfBGhvjHvP46CtW5j6POvhYGGo= -sigs.k8s.io/yaml v1.3.0/go.mod h1:GeOyir5tyXNByN85N/dRIT9es5UQNerPYEKK56eTBm8= diff --git a/cloud-mgmt/internal/event-processor/event-proccesor.go b/persys-federation/internal/event-processor/event-proccesor.go similarity index 100% rename from cloud-mgmt/internal/event-processor/event-proccesor.go rename to persys-federation/internal/event-processor/event-proccesor.go diff --git a/cloud-mgmt/internal/providers/azure/azure.go b/persys-federation/internal/providers/azure/azure.go similarity index 100% rename from cloud-mgmt/internal/providers/azure/azure.go rename to persys-federation/internal/providers/azure/azure.go diff --git a/persys-federation/internal/providers/persys/persys.go b/persys-federation/internal/providers/persys/persys.go new file mode 100644 index 0000000..59720d7 --- /dev/null +++ b/persys-federation/internal/providers/persys/persys.go @@ -0,0 +1,12 @@ +package providers + +// Minimal constants used by legacy integration tests. +const ( + CloudActionSuccess = "success" + CloudActionFailure = "failure" +) + +// CreateCluster is currently a placeholder hook for provider-backed cluster provisioning. +func CreateCluster() error { + return nil +} diff --git a/persys-federation/internal/providers/provider.go b/persys-federation/internal/providers/provider.go new file mode 100644 index 0000000..fddf508 --- /dev/null +++ b/persys-federation/internal/providers/provider.go @@ -0,0 +1,167 @@ +package providers + +import ( + "context" + "fmt" + + "github.com/hashicorp/terraform-exec/tfexec" + "github.com/persys-dev/persys-cloud/persys-federation/config" + "github.com/redis/go-redis/v9" +) + +type Provider interface { + Provision(ctx context.Context, envID string, spec *EnvironmentSpec) error + Update(ctx context.Context, envID string, spec *EnvironmentSpec) error + Delete(ctx context.Context, envID string) error +} + +type Manager struct { + config *config.Config + redis *redis.Client + aws Provider + azure Provider + gcp Provider + vsphere Provider + onprem Provider +} + +type EnvironmentSpec struct { + Provider string `json:"provider"` + ClusterName string `json:"cluster_name"` + Namespace string `json:"namespace"` + K8sVersion string `json:"k8s_version"` + NodeCount int `json:"node_count"` + Region string `json:"region"` + Credentials map[string]string `json:"credentials"` + InfraConfig map[string]string `json:"infra_config"` +} + +func NewManager(config *config.Config, redis *redis.Client) *Manager { + return &Manager{ + config: config, + redis: redis, + aws: &AWSProvider{config: config}, + azure: &AzureProvider{config: config}, + gcp: &GCPProvider{config: config}, + vsphere: &VSphereProvider{config: config}, + onprem: &OnPremProvider{config: config}, + } +} + +func (m *Manager) GetProvider(provider string) (Provider, error) { + switch provider { + case "aws": + return m.aws, nil + case "azure": + return m.azure, nil + case "gcp": + return m.gcp, nil + case "vsphere": + return m.vsphere, nil + case "onprem": + return m.onprem, nil + default: + return nil, fmt.Errorf("unsupported provider: %s", provider) + } +} + +type AWSProvider struct { + config *config.Config +} + +func (p *AWSProvider) Provision(ctx context.Context, envID string, spec *EnvironmentSpec) error { + // Initialize Terraform + tf, err := tfexec.NewTerraform("/path/to/terraform", "/path/to/terraform") + if err != nil { + return err + } + _ = tf + // Apply Terraform for VPC, subnets, etc. + // Example: tf.Apply(ctx, tfexec.Dir("/path/to/aws_infra")) + + // TODO: wire concrete provider-specific CAPI resources. + cluster := map[string]interface{}{} + // Apply CAPI resources + return applyCAPICluster(ctx, cluster, spec) +} + +func (p *AWSProvider) Update(ctx context.Context, envID string, spec *EnvironmentSpec) error { + return nil +} + +func (p *AWSProvider) Delete(ctx context.Context, envID string) error { + return nil +} + +type AzureProvider struct { + config *config.Config +} + +func (p *AzureProvider) Provision(ctx context.Context, envID string, spec *EnvironmentSpec) error { + // Similar logic for Azure using CAPZ + return nil +} + +func (p *AzureProvider) Update(ctx context.Context, envID string, spec *EnvironmentSpec) error { + return nil +} + +func (p *AzureProvider) Delete(ctx context.Context, envID string) error { + return nil +} + +type GCPProvider struct { + config *config.Config +} + +func (p *GCPProvider) Provision(ctx context.Context, envID string, spec *EnvironmentSpec) error { + return nil +} + +func (p *GCPProvider) Update(ctx context.Context, envID string, spec *EnvironmentSpec) error { + return nil +} + +func (p *GCPProvider) Delete(ctx context.Context, envID string) error { + return nil +} + +type VSphereProvider struct { + config *config.Config +} + +func (p *VSphereProvider) Provision(ctx context.Context, envID string, spec *EnvironmentSpec) error { + return nil +} + +func (p *VSphereProvider) Update(ctx context.Context, envID string, spec *EnvironmentSpec) error { + return nil +} + +func (p *VSphereProvider) Delete(ctx context.Context, envID string) error { + return nil +} + +type OnPremProvider struct { + config *config.Config +} + +func (p *OnPremProvider) Provision(ctx context.Context, envID string, spec *EnvironmentSpec) error { + return nil +} + +func (p *OnPremProvider) Update(ctx context.Context, envID string, spec *EnvironmentSpec) error { + return nil +} + +func (p *OnPremProvider) Delete(ctx context.Context, envID string) error { + return nil +} + +// Implement GCPProvider, VSphereProvider, OnPremProvider similarly + +func applyCAPICluster(ctx context.Context, cluster interface{}, spec *EnvironmentSpec) error { + // Use Kubernetes client to apply CAPI resources + // Similar to persys.CreateCluster but generalized + return nil +} diff --git a/cloud-mgmt/models/cloud.model.go b/persys-federation/models/cloud.model.go similarity index 100% rename from cloud-mgmt/models/cloud.model.go rename to persys-federation/models/cloud.model.go diff --git a/cloud-mgmt/pkg/mongodb/mongodb.go b/persys-federation/pkg/mongodb/mongodb.go similarity index 100% rename from cloud-mgmt/pkg/mongodb/mongodb.go rename to persys-federation/pkg/mongodb/mongodb.go diff --git a/cloud-mgmt/proto/cloud-mgmt.pb.go b/persys-federation/proto/cloud-mgmt.pb.go similarity index 100% rename from cloud-mgmt/proto/cloud-mgmt.pb.go rename to persys-federation/proto/cloud-mgmt.pb.go diff --git a/cloud-mgmt/proto/cloud-mgmt.proto b/persys-federation/proto/cloud-mgmt.proto similarity index 100% rename from cloud-mgmt/proto/cloud-mgmt.proto rename to persys-federation/proto/cloud-mgmt.proto diff --git a/cloud-mgmt/proto/cloud-mgmt_grpc.pb.go b/persys-federation/proto/cloud-mgmt_grpc.pb.go similarity index 100% rename from cloud-mgmt/proto/cloud-mgmt_grpc.pb.go rename to persys-federation/proto/cloud-mgmt_grpc.pb.go diff --git a/persys-federation/services/cloud.service.factory.go b/persys-federation/services/cloud.service.factory.go new file mode 100644 index 0000000..840da14 --- /dev/null +++ b/persys-federation/services/cloud.service.factory.go @@ -0,0 +1,15 @@ +package services + +import ( + "context" + + "go.mongodb.org/mongo-driver/mongo" +) + +type cloudServiceImpl struct{} + +// NewCloudService returns a no-op CloudService implementation. +// This keeps the service wiring compile-safe while CloudService methods are still being defined. +func NewCloudService(_ *mongo.Database, _ context.Context, _ ...any) CloudService { + return &cloudServiceImpl{} +} diff --git a/cloud-mgmt/services/cloud.service.go b/persys-federation/services/cloud.service.go similarity index 100% rename from cloud-mgmt/services/cloud.service.go rename to persys-federation/services/cloud.service.go diff --git a/cloud-mgmt/services/cloud.service.impl.go b/persys-federation/services/cloud.service.impl.go similarity index 100% rename from cloud-mgmt/services/cloud.service.impl.go rename to persys-federation/services/cloud.service.impl.go diff --git a/persys-federation/tests/suite_test.go b/persys-federation/tests/suite_test.go new file mode 100644 index 0000000..f89149c --- /dev/null +++ b/persys-federation/tests/suite_test.go @@ -0,0 +1,9 @@ +package tests + +import "testing" + +// Integration coverage in this module is currently being redesigned. +// Keep a compile-safe placeholder test to avoid stale test harness breakage. +func TestFederationSuitePlaceholder(t *testing.T) { + t.Skip("federation integration suite is not yet implemented") +} diff --git a/cloud-mgmt/utils/audit.go b/persys-federation/utils/audit.go similarity index 100% rename from cloud-mgmt/utils/audit.go rename to persys-federation/utils/audit.go diff --git a/cloud-mgmt/utils/blob.go b/persys-federation/utils/blob.go similarity index 100% rename from cloud-mgmt/utils/blob.go rename to persys-federation/utils/blob.go diff --git a/persys-forgery/Dockerfile b/persys-forgery/Dockerfile new file mode 100644 index 0000000..a062816 --- /dev/null +++ b/persys-forgery/Dockerfile @@ -0,0 +1,13 @@ +# syntax=docker/dockerfile:1 + +FROM golang:1.22-alpine AS builder +WORKDIR /app +COPY . . +RUN go build -o persys-forge ./cmd/main.go + +FROM alpine:3.19 +WORKDIR /app +COPY --from=builder /app/persys-forge /app/persys-forge +COPY config.yaml /app/config.yaml +EXPOSE 8080 +CMD ["/app/persys-forge"] \ No newline at end of file diff --git a/persys-forgery/Makefile b/persys-forgery/Makefile new file mode 100644 index 0000000..77dcb0d --- /dev/null +++ b/persys-forgery/Makefile @@ -0,0 +1,22 @@ +APP=persys-forgery +BINARY=bin/$(APP) +DOCKER_IMAGE=persys-forgery:latest + +.PHONY: all build test docker-build docker-run clean + +all: build + +build: + go build -o $(BINARY) ./cmd/main.go + +test: + go test -v ./tests/... + +docker-build: + docker build -t $(DOCKER_IMAGE) . + +docker-run: + docker run --rm -p 8080:8080 -v $(PWD)/config.yaml:/app/config.yaml $(DOCKER_IMAGE) + +clean: + rm -rf bin/ \ No newline at end of file diff --git a/persys-forgery/cmd/main.go b/persys-forgery/cmd/main.go new file mode 100644 index 0000000..1140101 --- /dev/null +++ b/persys-forgery/cmd/main.go @@ -0,0 +1,135 @@ +package main + +import ( + "context" + "fmt" + "log" + + "net/http" + + "github.com/gin-gonic/gin" + "github.com/persys-dev/persys-cloud/persys-forgery/internal/build" + "github.com/persys-dev/persys-cloud/persys-forgery/internal/db" + "github.com/persys-dev/persys-cloud/persys-forgery/internal/handlers" + "github.com/persys-dev/persys-cloud/persys-forgery/internal/operator" + "github.com/persys-dev/persys-cloud/persys-forgery/internal/queue" + secretsPkg "github.com/persys-dev/persys-cloud/persys-forgery/internal/secrets" + "github.com/persys-dev/persys-cloud/persys-forgery/utils" + "github.com/prometheus/client_golang/prometheus/promhttp" + gootelgin "go.opentelemetry.io/contrib/instrumentation/github.com/gin-gonic/gin/otelgin" + "go.opentelemetry.io/otel" + "go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp" + otelprom "go.opentelemetry.io/otel/exporters/prometheus" + "go.opentelemetry.io/otel/sdk/metric" + "go.opentelemetry.io/otel/sdk/resource" + "go.opentelemetry.io/otel/sdk/trace" + semconv "go.opentelemetry.io/otel/semconv/v1.21.0" +) + +func setupObservability(ctx context.Context) (func(), error) { + // OTLP HTTP exporter (replaces Jaeger) + otlpExp, err := otlptracehttp.New(ctx, otlptracehttp.WithEndpoint("localhost:4318"), otlptracehttp.WithInsecure()) + if err != nil { + return nil, err + } + // Prometheus exporter + promExp, err := otelprom.New() + if err != nil { + return nil, err + } + res, _ := resource.Merge( + resource.Default(), + resource.NewWithAttributes( + semconv.SchemaURL, + semconv.ServiceName("persys-forge"), + ), + ) + tracerProvider := trace.NewTracerProvider( + trace.WithBatcher(otlpExp), + trace.WithResource(res), + ) + otel.SetTracerProvider(tracerProvider) + meterProvider := metric.NewMeterProvider( + metric.WithReader(promExp), + metric.WithResource(res), + ) + otel.SetMeterProvider(meterProvider) + // Expose /metrics endpoint + go func() { + http.Handle("/metrics", promhttp.Handler()) + http.ListenAndServe(":2112", nil) + }() + return func() { + _ = tracerProvider.Shutdown(ctx) + _ = meterProvider.Shutdown(ctx) + }, nil +} + +func main() { + ctx := context.Background() + cleanup, err := setupObservability(ctx) + if err != nil { + log.Fatalf("failed to set up observability: %v", err) + } + defer cleanup() + + cfg, err := utils.LoadConfig("config.yaml") + if err != nil { + log.Fatalf("failed to load config: %v", err) + } + + if err := db.InitMySQL(cfg.MySQLDSN); err != nil { + log.Fatalf("failed to connect to MySQL: %v", err) + } + + secretsMgr := &secretsPkg.Manager{DB: db.DB} + operatorClient := &operator.Client{} + + // Initialize orchestrator with proper clients + workspaceDir := cfg.Build.Workspace + if workspaceDir == "" { + workspaceDir = "/tmp/forge-builds" + } + orchestrator := build.NewOrchestrator(workspaceDir) + orchestrator.SetOperatorClient(operatorClient) + + // Initialize Docker client for local builds + dockerClient := &build.Docker{} + orchestrator.SetDockerClient(dockerClient) + + // Start Redis worker for build jobs + go queue.StartRedisWorker(cfg, orchestrator) + + // Gin server setup + r := gin.Default() + r.Use(gootelgin.Middleware("persys-forge")) + + // Handlers + buildHandler := handlers.NewBuildHandler(orchestrator) + projectHandler := handlers.NewProjectHandler(db.DB) + secretHandler := handlers.NewSecretHandler(secretsMgr) + + // Build endpoints + r.POST("/build", buildHandler.Build) + r.GET("/status/:job_id", buildHandler.Status) + + // Project endpoints + r.POST("/projects", projectHandler.CreateProject) + r.GET("/projects", projectHandler.ListProjects) + r.GET("/projects/:project", projectHandler.GetProject) + r.PUT("/projects/:project", projectHandler.UpdateProject) + r.DELETE("/projects/:project", projectHandler.DeleteProject) + + // Secret endpoints + r.POST("/secrets", secretHandler.SetSecret) + // Project secrets (more specific first) + r.GET("/secrets/:project/:key", secretHandler.GetProjectSecret) + r.GET("/secrets/:project", secretHandler.ListProjectSecrets) + + // Start server + addr := fmt.Sprintf(":%d", cfg.Port) + log.Printf("Starting server on %s", addr) + if err := r.Run(addr); err != nil { + log.Fatalf("failed to start server: %v", err) + } +} diff --git a/persys-forgery/config.yaml b/persys-forgery/config.yaml new file mode 100644 index 0000000..792af78 --- /dev/null +++ b/persys-forgery/config.yaml @@ -0,0 +1,13 @@ +# Sample config.yaml for persys-forge +port: 8087 +mysql_dsn: "forgery:password@tcp(localhost:3306)/forgery_db?parseTime=true" +vault_addr: "http://localhost:8200" +traefik_url: "http://traefik.local" +nexus_url: "http://nexus.local:8081" +nexus_user: "nexususer" +nexus_password: "nexuspass" +nexus_docker_repo: "nexus.local:8082/repository/docker-releases" +redis: + addr: "localhost:6379" + password: "" + db: 0 \ No newline at end of file diff --git a/persys-forgery/go.mod b/persys-forgery/go.mod new file mode 100644 index 0000000..f8496e5 --- /dev/null +++ b/persys-forgery/go.mod @@ -0,0 +1,91 @@ +module github.com/persys-dev/persys-cloud/persys-forgery + +go 1.24.2 + +require ( + github.com/docker/docker v28.3.2+incompatible + github.com/gin-gonic/gin v1.10.1 + github.com/prometheus/client_golang v1.22.0 + github.com/redis/go-redis/v9 v9.11.0 + go.opentelemetry.io/contrib/instrumentation/github.com/gin-gonic/gin/otelgin v0.62.0 + go.opentelemetry.io/otel v1.37.0 + go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.37.0 + go.opentelemetry.io/otel/exporters/prometheus v0.59.0 + go.opentelemetry.io/otel/sdk v1.37.0 + go.opentelemetry.io/otel/sdk/metric v1.37.0 + gopkg.in/yaml.v3 v3.0.1 + gorm.io/driver/mysql v1.6.0 + gorm.io/driver/sqlite v1.6.0 + gorm.io/gorm v1.30.0 +) + +require ( + filippo.io/edwards25519 v1.1.0 // indirect + github.com/Microsoft/go-winio v0.4.14 // indirect + github.com/beorn7/perks v1.0.1 // indirect + github.com/bytedance/sonic v1.13.3 // indirect + github.com/bytedance/sonic/loader v0.2.4 // indirect + github.com/cenkalti/backoff/v5 v5.0.2 // indirect + github.com/cespare/xxhash/v2 v2.3.0 // indirect + github.com/cloudwego/base64x v0.1.5 // indirect + github.com/containerd/errdefs v1.0.0 // indirect + github.com/containerd/errdefs/pkg v0.3.0 // indirect + github.com/containerd/log v0.1.0 // indirect + github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f // indirect + github.com/distribution/reference v0.6.0 // indirect + github.com/docker/go-connections v0.5.0 // indirect + github.com/docker/go-units v0.5.0 // indirect + github.com/felixge/httpsnoop v1.0.4 // indirect + github.com/gabriel-vasile/mimetype v1.4.9 // indirect + github.com/gin-contrib/sse v1.1.0 // indirect + github.com/go-logr/logr v1.4.3 // indirect + github.com/go-logr/stdr v1.2.2 // indirect + github.com/go-playground/locales v0.14.1 // indirect + github.com/go-playground/universal-translator v0.18.1 // indirect + github.com/go-playground/validator/v10 v10.26.0 // indirect + github.com/go-sql-driver/mysql v1.8.1 // indirect + github.com/goccy/go-json v0.10.5 // indirect + github.com/gogo/protobuf v1.3.2 // indirect + github.com/google/uuid v1.6.0 // indirect + github.com/grpc-ecosystem/grpc-gateway/v2 v2.27.1 // indirect + github.com/jinzhu/inflection v1.0.0 // indirect + github.com/jinzhu/now v1.1.5 // indirect + github.com/json-iterator/go v1.1.12 // indirect + github.com/klauspost/cpuid/v2 v2.2.11 // indirect + github.com/leodido/go-urn v1.4.0 // indirect + github.com/mattn/go-isatty v0.0.20 // indirect + github.com/mattn/go-sqlite3 v1.14.22 // indirect + github.com/moby/docker-image-spec v1.3.1 // indirect + github.com/moby/sys/atomicwriter v0.1.0 // indirect + github.com/moby/term v0.5.2 // indirect + github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect + github.com/modern-go/reflect2 v1.0.2 // indirect + github.com/morikuni/aec v1.0.0 // indirect + github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect + github.com/opencontainers/go-digest v1.0.0 // indirect + github.com/opencontainers/image-spec v1.1.1 // indirect + github.com/pelletier/go-toml/v2 v2.2.4 // indirect + github.com/pkg/errors v0.9.1 // indirect + github.com/prometheus/client_model v0.6.2 // indirect + github.com/prometheus/common v0.65.0 // indirect + github.com/prometheus/procfs v0.16.1 // indirect + github.com/twitchyliquid64/golang-asm v0.15.1 // indirect + github.com/ugorji/go/codec v1.3.0 // indirect + go.opentelemetry.io/auto/sdk v1.1.0 // indirect + go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.62.0 // indirect + go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.37.0 // indirect + go.opentelemetry.io/otel/metric v1.37.0 // indirect + go.opentelemetry.io/otel/trace v1.37.0 // indirect + go.opentelemetry.io/proto/otlp v1.7.0 // indirect + golang.org/x/arch v0.18.0 // indirect + golang.org/x/crypto v0.39.0 // indirect + golang.org/x/net v0.41.0 // indirect + golang.org/x/sys v0.33.0 // indirect + golang.org/x/text v0.26.0 // indirect + golang.org/x/time v0.12.0 // indirect + google.golang.org/genproto/googleapis/api v0.0.0-20250603155806-513f23925822 // indirect + google.golang.org/genproto/googleapis/rpc v0.0.0-20250603155806-513f23925822 // indirect + google.golang.org/grpc v1.73.0 // indirect + google.golang.org/protobuf v1.36.6 // indirect + gotest.tools/v3 v3.5.2 // indirect +) diff --git a/persys-forgery/go.sum b/persys-forgery/go.sum new file mode 100644 index 0000000..cdbadb9 --- /dev/null +++ b/persys-forgery/go.sum @@ -0,0 +1,260 @@ +filippo.io/edwards25519 v1.1.0 h1:FNf4tywRC1HmFuKW5xopWpigGjJKiJSV0Cqo0cJWDaA= +filippo.io/edwards25519 v1.1.0/go.mod h1:BxyFTGdWcka3PhytdK4V28tE5sGfRvvvRV7EaN4VDT4= +github.com/Azure/go-ansiterm v0.0.0-20250102033503-faa5f7b0171c h1:udKWzYgxTojEKWjV8V+WSxDXJ4NFATAsZjh8iIbsQIg= +github.com/Azure/go-ansiterm v0.0.0-20250102033503-faa5f7b0171c/go.mod h1:xomTg63KZ2rFqZQzSB4Vz2SUXa1BpHTVz9L5PTmPC4E= +github.com/Microsoft/go-winio v0.4.14 h1:+hMXMk01us9KgxGb7ftKQt2Xpf5hH/yky+TDA+qxleU= +github.com/Microsoft/go-winio v0.4.14/go.mod h1:qXqCSQ3Xa7+6tgxaGTIe4Kpcdsi+P8jBhyzoq1bpyYA= +github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM= +github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw= +github.com/bsm/ginkgo/v2 v2.12.0 h1:Ny8MWAHyOepLGlLKYmXG4IEkioBysk6GpaRTLC8zwWs= +github.com/bsm/ginkgo/v2 v2.12.0/go.mod h1:SwYbGRRDovPVboqFv0tPTcG1sN61LM1Z4ARdbAV9g4c= +github.com/bsm/gomega v1.27.10 h1:yeMWxP2pV2fG3FgAODIY8EiRE3dy0aeFYt4l7wh6yKA= +github.com/bsm/gomega v1.27.10/go.mod h1:JyEr/xRbxbtgWNi8tIEVPUYZ5Dzef52k01W3YH0H+O0= +github.com/bytedance/sonic v1.13.3 h1:MS8gmaH16Gtirygw7jV91pDCN33NyMrPbN7qiYhEsF0= +github.com/bytedance/sonic v1.13.3/go.mod h1:o68xyaF9u2gvVBuGHPlUVCy+ZfmNNO5ETf1+KgkJhz4= +github.com/bytedance/sonic/loader v0.1.1/go.mod h1:ncP89zfokxS5LZrJxl5z0UJcsk4M4yY2JpfqGeCtNLU= +github.com/bytedance/sonic/loader v0.2.4 h1:ZWCw4stuXUsn1/+zQDqeE7JKP+QO47tz7QCNan80NzY= +github.com/bytedance/sonic/loader v0.2.4/go.mod h1:N8A3vUdtUebEY2/VQC0MyhYeKUFosQU6FxH2JmUe6VI= +github.com/cenkalti/backoff/v5 v5.0.2 h1:rIfFVxEf1QsI7E1ZHfp/B4DF/6QBAUhmgkxc0H7Zss8= +github.com/cenkalti/backoff/v5 v5.0.2/go.mod h1:rkhZdG3JZukswDf7f0cwqPNk4K0sa+F97BxZthm/crw= +github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs= +github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= +github.com/cloudwego/base64x v0.1.5 h1:XPciSp1xaq2VCSt6lF0phncD4koWyULpl5bUxbfCyP4= +github.com/cloudwego/base64x v0.1.5/go.mod h1:0zlkT4Wn5C6NdauXdJRhSKRlJvmclQ1hhJgA0rcu/8w= +github.com/cloudwego/iasm v0.2.0/go.mod h1:8rXZaNYT2n95jn+zTI1sDr+IgcD2GVs0nlbbQPiEFhY= +github.com/containerd/errdefs v1.0.0 h1:tg5yIfIlQIrxYtu9ajqY42W3lpS19XqdxRQeEwYG8PI= +github.com/containerd/errdefs v1.0.0/go.mod h1:+YBYIdtsnF4Iw6nWZhJcqGSg/dwvV7tyJ/kCkyJ2k+M= +github.com/containerd/errdefs/pkg v0.3.0 h1:9IKJ06FvyNlexW690DXuQNx2KA2cUJXx151Xdx3ZPPE= +github.com/containerd/errdefs/pkg v0.3.0/go.mod h1:NJw6s9HwNuRhnjJhM7pylWwMyAkmCQvQ4GpJHEqRLVk= +github.com/containerd/log v0.1.0 h1:TCJt7ioM2cr/tfR8GPbGf9/VRAX8D2B4PjzCpfX540I= +github.com/containerd/log v0.1.0/go.mod h1:VRRf09a7mHDIRezVKTRCrOq78v577GXq3bSa3EhrzVo= +github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= +github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f h1:lO4WD4F/rVNCu3HqELle0jiPLLBs70cWOduZpkS1E78= +github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f/go.mod h1:cuUVRXasLTGF7a8hSLbxyZXjz+1KgoB3wDUb6vlszIc= +github.com/distribution/reference v0.6.0 h1:0IXCQ5g4/QMHHkarYzh5l+u8T3t73zM5QvfrDyIgxBk= +github.com/distribution/reference v0.6.0/go.mod h1:BbU0aIcezP1/5jX/8MP0YiH4SdvB5Y4f/wlDRiLyi3E= +github.com/docker/docker v28.3.2+incompatible h1:wn66NJ6pWB1vBZIilP8G3qQPqHy5XymfYn5vsqeA5oA= +github.com/docker/docker v28.3.2+incompatible/go.mod h1:eEKB0N0r5NX/I1kEveEz05bcu8tLC/8azJZsviup8Sk= +github.com/docker/go-connections v0.5.0 h1:USnMq7hx7gwdVZq1L49hLXaFtUdTADjXGp+uj1Br63c= +github.com/docker/go-connections v0.5.0/go.mod h1:ov60Kzw0kKElRwhNs9UlUHAE/F9Fe6GLaXnqyDdmEXc= +github.com/docker/go-units v0.5.0 h1:69rxXcBk27SvSaaxTtLh/8llcHD8vYHT7WSdRZ/jvr4= +github.com/docker/go-units v0.5.0/go.mod h1:fgPhTUdO+D/Jk86RDLlptpiXQzgHJF7gydDDbaIK4Dk= +github.com/felixge/httpsnoop v1.0.4 h1:NFTV2Zj1bL4mc9sqWACXbQFVBBg2W3GPvqp8/ESS2Wg= +github.com/felixge/httpsnoop v1.0.4/go.mod h1:m8KPJKqk1gH5J9DgRY2ASl2lWCfGKXixSwevea8zH2U= +github.com/gabriel-vasile/mimetype v1.4.9 h1:5k+WDwEsD9eTLL8Tz3L0VnmVh9QxGjRmjBvAG7U/oYY= +github.com/gabriel-vasile/mimetype v1.4.9/go.mod h1:WnSQhFKJuBlRyLiKohA/2DtIlPFAbguNaG7QCHcyGok= +github.com/gin-contrib/sse v1.1.0 h1:n0w2GMuUpWDVp7qSpvze6fAu9iRxJY4Hmj6AmBOU05w= +github.com/gin-contrib/sse v1.1.0/go.mod h1:hxRZ5gVpWMT7Z0B0gSNYqqsSCNIJMjzvm6fqCz9vjwM= +github.com/gin-gonic/gin v1.10.1 h1:T0ujvqyCSqRopADpgPgiTT63DUQVSfojyME59Ei63pQ= +github.com/gin-gonic/gin v1.10.1/go.mod h1:4PMNQiOhvDRa013RKVbsiNwoyezlm2rm0uX/T7kzp5Y= +github.com/go-logr/logr v1.2.2/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A= +github.com/go-logr/logr v1.4.3 h1:CjnDlHq8ikf6E492q6eKboGOC0T8CDaOvkHCIg8idEI= +github.com/go-logr/logr v1.4.3/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY= +github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag= +github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE= +github.com/go-playground/assert/v2 v2.2.0 h1:JvknZsQTYeFEAhQwI4qEt9cyV5ONwRHC+lYKSsYSR8s= +github.com/go-playground/assert/v2 v2.2.0/go.mod h1:VDjEfimB/XKnb+ZQfWdccd7VUvScMdVu0Titje2rxJ4= +github.com/go-playground/locales v0.14.1 h1:EWaQ/wswjilfKLTECiXz7Rh+3BjFhfDFKv/oXslEjJA= +github.com/go-playground/locales v0.14.1/go.mod h1:hxrqLVvrK65+Rwrd5Fc6F2O76J/NuW9t0sjnWqG1slY= +github.com/go-playground/universal-translator v0.18.1 h1:Bcnm0ZwsGyWbCzImXv+pAJnYK9S473LQFuzCbDbfSFY= +github.com/go-playground/universal-translator v0.18.1/go.mod h1:xekY+UJKNuX9WP91TpwSH2VMlDf28Uj24BCp08ZFTUY= +github.com/go-playground/validator/v10 v10.26.0 h1:SP05Nqhjcvz81uJaRfEV0YBSSSGMc/iMaVtFbr3Sw2k= +github.com/go-playground/validator/v10 v10.26.0/go.mod h1:I5QpIEbmr8On7W0TktmJAumgzX4CA1XNl4ZmDuVHKKo= +github.com/go-sql-driver/mysql v1.8.1 h1:LedoTUt/eveggdHS9qUFC1EFSa8bU2+1pZjSRpvNJ1Y= +github.com/go-sql-driver/mysql v1.8.1/go.mod h1:wEBSXgmK//2ZFJyE+qWnIsVGmvmEKlqwuVSjsCm7DZg= +github.com/goccy/go-json v0.10.5 h1:Fq85nIqj+gXn/S5ahsiTlK3TmC85qgirsdTP/+DeaC4= +github.com/goccy/go-json v0.10.5/go.mod h1:oq7eo15ShAhp70Anwd5lgX2pLfOS3QCiwU/PULtXL6M= +github.com/gogo/protobuf v1.3.2 h1:Ov1cvc58UF3b5XjBnZv7+opcTcQFZebYjWzi34vdm4Q= +github.com/gogo/protobuf v1.3.2/go.mod h1:P1XiOD3dCwIKUDQYPy72D8LYyHL2YPYrpS2s69NZV8Q= +github.com/golang/protobuf v1.5.4 h1:i7eJL8qZTpSEXOPTxNKhASYpMn+8e5Q6AdndVa1dWek= +github.com/golang/protobuf v1.5.4/go.mod h1:lnTiLA8Wa4RWRcIUkrtSVa5nRhsEGBg48fD6rSs7xps= +github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8= +github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU= +github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= +github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= +github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= +github.com/grpc-ecosystem/grpc-gateway/v2 v2.27.1 h1:X5VWvz21y3gzm9Nw/kaUeku/1+uBhcekkmy4IkffJww= +github.com/grpc-ecosystem/grpc-gateway/v2 v2.27.1/go.mod h1:Zanoh4+gvIgluNqcfMVTJueD4wSS5hT7zTt4Mrutd90= +github.com/jinzhu/inflection v1.0.0 h1:K317FqzuhWc8YvSVlFMCCUb36O/S9MCKRDI7QkRKD/E= +github.com/jinzhu/inflection v1.0.0/go.mod h1:h+uFLlag+Qp1Va5pdKtLDYj+kHp5pxUVkryuEj+Srlc= +github.com/jinzhu/now v1.1.5 h1:/o9tlHleP7gOFmsnYNz3RGnqzefHA47wQpKrrdTIwXQ= +github.com/jinzhu/now v1.1.5/go.mod h1:d3SSVoowX0Lcu0IBviAWJpolVfI5UJVZZ7cO71lE/z8= +github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM= +github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo= +github.com/kisielk/errcheck v1.5.0/go.mod h1:pFxgyoBC7bSaBwPgfKdkLd5X25qrDl4LWUI2bnpBCr8= +github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck= +github.com/klauspost/compress v1.18.0 h1:c/Cqfb0r+Yi+JtIEq73FWXVkRonBlf0CRNYc8Zttxdo= +github.com/klauspost/compress v1.18.0/go.mod h1:2Pp+KzxcywXVXMr50+X0Q/Lsb43OQHYWRCY2AiWywWQ= +github.com/klauspost/cpuid/v2 v2.0.9/go.mod h1:FInQzS24/EEf25PyTYn52gqo7WaD8xa0213Md/qVLRg= +github.com/klauspost/cpuid/v2 v2.2.11 h1:0OwqZRYI2rFrjS4kvkDnqJkKHdHaRnCm68/DY4OxRzU= +github.com/klauspost/cpuid/v2 v2.2.11/go.mod h1:hqwkgyIinND0mEev00jJYCxPNVRVXFQeu1XKlok6oO0= +github.com/knz/go-libedit v1.10.1/go.mod h1:MZTVkCWyz0oBc7JOWP3wNAzd002ZbM/5hgShxwh4x8M= +github.com/konsorten/go-windows-terminal-sequences v1.0.1/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ= +github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE= +github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk= +github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= +github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= +github.com/kylelemons/godebug v1.1.0 h1:RPNrshWIDI6G2gRW9EHilWtl7Z6Sb1BR0xunSBf0SNc= +github.com/kylelemons/godebug v1.1.0/go.mod h1:9/0rRGxNHcop5bhtWyNeEfOS8JIWk580+fNqagV/RAw= +github.com/leodido/go-urn v1.4.0 h1:WT9HwE9SGECu3lg4d/dIA+jxlljEa1/ffXKmRjqdmIQ= +github.com/leodido/go-urn v1.4.0/go.mod h1:bvxc+MVxLKB4z00jd1z+Dvzr47oO32F/QSNjSBOlFxI= +github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY= +github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= +github.com/mattn/go-sqlite3 v1.14.22 h1:2gZY6PC6kBnID23Tichd1K+Z0oS6nE/XwU+Vz/5o4kU= +github.com/mattn/go-sqlite3 v1.14.22/go.mod h1:Uh1q+B4BYcTPb+yiD3kU8Ct7aC0hY9fxUwlHK0RXw+Y= +github.com/moby/docker-image-spec v1.3.1 h1:jMKff3w6PgbfSa69GfNg+zN/XLhfXJGnEx3Nl2EsFP0= +github.com/moby/docker-image-spec v1.3.1/go.mod h1:eKmb5VW8vQEh/BAr2yvVNvuiJuY6UIocYsFu/DxxRpo= +github.com/moby/sys/atomicwriter v0.1.0 h1:kw5D/EqkBwsBFi0ss9v1VG3wIkVhzGvLklJ+w3A14Sw= +github.com/moby/sys/atomicwriter v0.1.0/go.mod h1:Ul8oqv2ZMNHOceF643P6FKPXeCmYtlQMvpizfsSoaWs= +github.com/moby/sys/sequential v0.6.0 h1:qrx7XFUd/5DxtqcoH1h438hF5TmOvzC/lspjy7zgvCU= +github.com/moby/sys/sequential v0.6.0/go.mod h1:uyv8EUTrca5PnDsdMGXhZe6CCe8U/UiTWd+lL+7b/Ko= +github.com/moby/term v0.5.2 h1:6qk3FJAFDs6i/q3W/pQ97SX192qKfZgGjCQqfCJkgzQ= +github.com/moby/term v0.5.2/go.mod h1:d3djjFCrjnB+fl8NJux+EJzu0msscUP+f8it8hPkFLc= +github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= +github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg= +github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= +github.com/modern-go/reflect2 v1.0.2 h1:xBagoLtFs94CBntxluKeaWgTMpvLxC4ur3nMaC9Gz0M= +github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk= +github.com/morikuni/aec v1.0.0 h1:nP9CBfwrvYnBRgY6qfDQkygYDmYwOilePFkwzv4dU8A= +github.com/morikuni/aec v1.0.0/go.mod h1:BbKIizmSmc5MMPqRYbxO4ZU0S0+P200+tUnFx7PXmsc= +github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 h1:C3w9PqII01/Oq1c1nUAm88MOHcQC9l5mIlSMApZMrHA= +github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822/go.mod h1:+n7T8mK8HuQTcFwEeznm/DIxMOiR9yIdICNftLE1DvQ= +github.com/opencontainers/go-digest v1.0.0 h1:apOUWs51W5PlhuyGyz9FCeeBIOUDA/6nW8Oi/yOhh5U= +github.com/opencontainers/go-digest v1.0.0/go.mod h1:0JzlMkj0TRzQZfJkVvzbP0HBR3IKzErnv2BNG4W4MAM= +github.com/opencontainers/image-spec v1.1.1 h1:y0fUlFfIZhPF1W537XOLg0/fcx6zcHCJwooC2xJA040= +github.com/opencontainers/image-spec v1.1.1/go.mod h1:qpqAh3Dmcf36wStyyWU+kCeDgrGnAve2nCC8+7h8Q0M= +github.com/pelletier/go-toml/v2 v2.2.4 h1:mye9XuhQ6gvn5h28+VilKrrPoQVanw5PMw/TB0t5Ec4= +github.com/pelletier/go-toml/v2 v2.2.4/go.mod h1:2gIqNv+qfxSVS7cM2xJQKtLSTLUE9V8t9Stt+h56mCY= +github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= +github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= +github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= +github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= +github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/prometheus/client_golang v1.22.0 h1:rb93p9lokFEsctTys46VnV1kLCDpVZ0a/Y92Vm0Zc6Q= +github.com/prometheus/client_golang v1.22.0/go.mod h1:R7ljNsLXhuQXYZYtw6GAE9AZg8Y7vEW5scdCXrWRXC0= +github.com/prometheus/client_model v0.6.2 h1:oBsgwpGs7iVziMvrGhE53c/GrLUsZdHnqNwqPLxwZyk= +github.com/prometheus/client_model v0.6.2/go.mod h1:y3m2F6Gdpfy6Ut/GBsUqTWZqCUvMVzSfMLjcu6wAwpE= +github.com/prometheus/common v0.65.0 h1:QDwzd+G1twt//Kwj/Ww6E9FQq1iVMmODnILtW1t2VzE= +github.com/prometheus/common v0.65.0/go.mod h1:0gZns+BLRQ3V6NdaerOhMbwwRbNh9hkGINtQAsP5GS8= +github.com/prometheus/procfs v0.16.1 h1:hZ15bTNuirocR6u0JZ6BAHHmwS1p8B4P6MRqxtzMyRg= +github.com/prometheus/procfs v0.16.1/go.mod h1:teAbpZRB1iIAJYREa1LsoWUXykVXA1KlTmWl8x/U+Is= +github.com/redis/go-redis/v9 v9.11.0 h1:E3S08Gl/nJNn5vkxd2i78wZxWAPNZgUNTp8WIJUAiIs= +github.com/redis/go-redis/v9 v9.11.0/go.mod h1:huWgSWd8mW6+m0VPhJjSSQ+d6Nh1VICQ6Q5lHuCH/Iw= +github.com/rogpeppe/go-internal v1.13.1 h1:KvO1DLK/DRN07sQ1LQKScxyZJuNnedQ5/wKSR38lUII= +github.com/rogpeppe/go-internal v1.13.1/go.mod h1:uMEvuHeurkdAXX61udpOXGD/AzZDWNMNyH2VO9fmH0o= +github.com/sirupsen/logrus v1.4.1/go.mod h1:ni0Sbl8bgC9z8RoU9G6nDWqqs/fq4eDPysMBDgk/93Q= +github.com/sirupsen/logrus v1.9.3 h1:dueUQJ1C2q9oE3F7wvmSGAaVtTmUizReu6fjN8uqzbQ= +github.com/sirupsen/logrus v1.9.3/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ= +github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= +github.com/stretchr/objx v0.1.1/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= +github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw= +github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo= +github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= +github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= +github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= +github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= +github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= +github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4= +github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA= +github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= +github.com/twitchyliquid64/golang-asm v0.15.1 h1:SU5vSMR7hnwNxj24w34ZyCi/FmDZTkS4MhqMhdFk5YI= +github.com/twitchyliquid64/golang-asm v0.15.1/go.mod h1:a1lVb/DtPvCB8fslRZhAngC2+aY1QWCk3Cedj/Gdt08= +github.com/ugorji/go/codec v1.3.0 h1:Qd2W2sQawAfG8XSvzwhBeoGq71zXOC/Q1E9y/wUcsUA= +github.com/ugorji/go/codec v1.3.0/go.mod h1:pRBVtBSKl77K30Bv8R2P+cLSGaTtex6fsA2Wjqmfxj4= +github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= +github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= +go.opentelemetry.io/auto/sdk v1.1.0 h1:cH53jehLUN6UFLY71z+NDOiNJqDdPRaXzTel0sJySYA= +go.opentelemetry.io/auto/sdk v1.1.0/go.mod h1:3wSPjt5PWp2RhlCcmmOial7AvC4DQqZb7a7wCow3W8A= +go.opentelemetry.io/contrib/instrumentation/github.com/gin-gonic/gin/otelgin v0.62.0 h1:fZNpsQuTwFFSGC96aJexNOBrCD7PjD9Tm/HyHtXhmnk= +go.opentelemetry.io/contrib/instrumentation/github.com/gin-gonic/gin/otelgin v0.62.0/go.mod h1:+NFxPSeYg0SoiRUO4k0ceJYMCY9FiRbYFmByUpm7GJY= +go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.62.0 h1:Hf9xI/XLML9ElpiHVDNwvqI0hIFlzV8dgIr35kV1kRU= +go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.62.0/go.mod h1:NfchwuyNoMcZ5MLHwPrODwUF1HWCXWrL31s8gSAdIKY= +go.opentelemetry.io/contrib/propagators/b3 v1.37.0 h1:0aGKdIuVhy5l4GClAjl72ntkZJhijf2wg1S7b5oLoYA= +go.opentelemetry.io/contrib/propagators/b3 v1.37.0/go.mod h1:nhyrxEJEOQdwR15zXrCKI6+cJK60PXAkJ/jRyfhr2mg= +go.opentelemetry.io/otel v1.37.0 h1:9zhNfelUvx0KBfu/gb+ZgeAfAgtWrfHJZcAqFC228wQ= +go.opentelemetry.io/otel v1.37.0/go.mod h1:ehE/umFRLnuLa/vSccNq9oS1ErUlkkK71gMcN34UG8I= +go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.37.0 h1:Ahq7pZmv87yiyn3jeFz/LekZmPLLdKejuO3NcK9MssM= +go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.37.0/go.mod h1:MJTqhM0im3mRLw1i8uGHnCvUEeS7VwRyxlLC78PA18M= +go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.37.0 h1:bDMKF3RUSxshZ5OjOTi8rsHGaPKsAt76FaqgvIUySLc= +go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.37.0/go.mod h1:dDT67G/IkA46Mr2l9Uj7HsQVwsjASyV9SjGofsiUZDA= +go.opentelemetry.io/otel/exporters/prometheus v0.59.0 h1:HHf+wKS6o5++XZhS98wvILrLVgHxjA/AMjqHKes+uzo= +go.opentelemetry.io/otel/exporters/prometheus v0.59.0/go.mod h1:R8GpRXTZrqvXHDEGVH5bF6+JqAZcK8PjJcZ5nGhEWiE= +go.opentelemetry.io/otel/exporters/stdout/stdouttrace v1.37.0 h1:SNhVp/9q4Go/XHBkQ1/d5u9P/U+L1yaGPoi0x+mStaI= +go.opentelemetry.io/otel/exporters/stdout/stdouttrace v1.37.0/go.mod h1:tx8OOlGH6R4kLV67YaYO44GFXloEjGPZuMjEkaaqIp4= +go.opentelemetry.io/otel/metric v1.37.0 h1:mvwbQS5m0tbmqML4NqK+e3aDiO02vsf/WgbsdpcPoZE= +go.opentelemetry.io/otel/metric v1.37.0/go.mod h1:04wGrZurHYKOc+RKeye86GwKiTb9FKm1WHtO+4EVr2E= +go.opentelemetry.io/otel/sdk v1.37.0 h1:ItB0QUqnjesGRvNcmAcU0LyvkVyGJ2xftD29bWdDvKI= +go.opentelemetry.io/otel/sdk v1.37.0/go.mod h1:VredYzxUvuo2q3WRcDnKDjbdvmO0sCzOvVAiY+yUkAg= +go.opentelemetry.io/otel/sdk/metric v1.37.0 h1:90lI228XrB9jCMuSdA0673aubgRobVZFhbjxHHspCPc= +go.opentelemetry.io/otel/sdk/metric v1.37.0/go.mod h1:cNen4ZWfiD37l5NhS+Keb5RXVWZWpRE+9WyVCpbo5ps= +go.opentelemetry.io/otel/trace v1.37.0 h1:HLdcFNbRQBE2imdSEgm/kwqmQj1Or1l/7bW6mxVK7z4= +go.opentelemetry.io/otel/trace v1.37.0/go.mod h1:TlgrlQ+PtQO5XFerSPUYG0JSgGyryXewPGyayAWSBS0= +go.opentelemetry.io/proto/otlp v1.7.0 h1:jX1VolD6nHuFzOYso2E73H85i92Mv8JQYk0K9vz09os= +go.opentelemetry.io/proto/otlp v1.7.0/go.mod h1:fSKjH6YJ7HDlwzltzyMj036AJ3ejJLCgCSHGj4efDDo= +go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto= +go.uber.org/goleak v1.3.0/go.mod h1:CoHD4mav9JJNrW/WLlf7HGZPjdw8EucARQHekz1X6bE= +golang.org/x/arch v0.18.0 h1:WN9poc33zL4AzGxqf8VtpKUnGvMi8O9lhNyBMF/85qc= +golang.org/x/arch v0.18.0/go.mod h1:bdwinDaKcfZUGpH09BB7ZmOfhalA8lQdzl62l8gGWsk= +golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= +golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= +golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= +golang.org/x/crypto v0.39.0 h1:SHs+kF4LP+f+p14esP5jAoDpHU8Gu/v9lFRK6IT5imM= +golang.org/x/crypto v0.39.0/go.mod h1:L+Xg3Wf6HoL4Bn4238Z6ft6KfEpN0tJGo53AAPC632U= +golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= +golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= +golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= +golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20200226121028-0de0cce0169b/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= +golang.org/x/net v0.41.0 h1:vBTly1HeNPEn3wtREYfy4GZ/NECgw2Cnl+nK6Nz3uvw= +golang.org/x/net v0.41.0/go.mod h1:B/K4NNqkfmg07DQYrbwvSluqCJOOXwUjeb/5lOisjbA= +golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sys v0.0.0-20180905080454-ebe1bf3edb33/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190507160741-ecd444e8653b/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.33.0 h1:q3i8TbbEz+JRD9ywIRlyRAQbM0qF7hu24q3teo2hbuw= +golang.org/x/sys v0.33.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k= +golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= +golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= +golang.org/x/text v0.26.0 h1:P42AVeLghgTYr4+xUnTRKDMqpar+PtX7KWuNQL21L8M= +golang.org/x/text v0.26.0/go.mod h1:QK15LZJUUQVJxhz7wXgxSy/CJaTFjd0G+YLonydOVQA= +golang.org/x/time v0.12.0 h1:ScB/8o8olJvc+CQPWrK3fPZNfh7qgwCrY0zJmoEQLSE= +golang.org/x/time v0.12.0/go.mod h1:CDIdPxbZBQxdj6cxyCIdrNogrJKMJ7pr37NYpMcMDSg= +golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= +golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.0.0-20200619180055-7c47624df98f/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= +golang.org/x/tools v0.0.0-20210106214847-113979e3529a/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= +golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +google.golang.org/genproto/googleapis/api v0.0.0-20250603155806-513f23925822 h1:oWVWY3NzT7KJppx2UKhKmzPq4SRe0LdCijVRwvGeikY= +google.golang.org/genproto/googleapis/api v0.0.0-20250603155806-513f23925822/go.mod h1:h3c4v36UTKzUiuaOKQ6gr3S+0hovBtUrXzTG/i3+XEc= +google.golang.org/genproto/googleapis/rpc v0.0.0-20250603155806-513f23925822 h1:fc6jSaCT0vBduLYZHYrBBNY4dsWuvgyff9noRNDdBeE= +google.golang.org/genproto/googleapis/rpc v0.0.0-20250603155806-513f23925822/go.mod h1:qQ0YXyHHx3XkvlzUtpXDkS29lDSafHMZBAZDc03LQ3A= +google.golang.org/grpc v1.73.0 h1:VIWSmpI2MegBtTuFt5/JWy2oXxtjJ/e89Z70ImfD2ok= +google.golang.org/grpc v1.73.0/go.mod h1:50sbHOUqWoCQGI8V2HQLJM0B+LMlIUjNSZmow7EVBQc= +google.golang.org/protobuf v1.36.6 h1:z1NpPI8ku2WgiWnf+t9wTPsn6eP1L7ksHUlkfLvd9xY= +google.golang.org/protobuf v1.36.6/go.mod h1:jduwjTPXsFjZGTmRluh+L6NjiWu7pchiJ2/5YcXBHnY= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk= +gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q= +gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= +gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +gorm.io/driver/mysql v1.6.0 h1:eNbLmNTpPpTOVZi8MMxCi2aaIm0ZpInbORNXDwyLGvg= +gorm.io/driver/mysql v1.6.0/go.mod h1:D/oCC2GWK3M/dqoLxnOlaNKmXz8WNTfcS9y5ovaSqKo= +gorm.io/driver/sqlite v1.6.0 h1:WHRRrIiulaPiPFmDcod6prc4l2VGVWHz80KspNsxSfQ= +gorm.io/driver/sqlite v1.6.0/go.mod h1:AO9V1qIQddBESngQUKWL9yoH93HIeA1X6V633rBwyT8= +gorm.io/gorm v1.30.0 h1:qbT5aPv1UH8gI99OsRlvDToLxW5zR7FzS9acZDOZcgs= +gorm.io/gorm v1.30.0/go.mod h1:8Z33v652h4//uMA76KjeDH8mJXPm1QNCYrMeatR0DOE= +gotest.tools/v3 v3.5.2 h1:7koQfIKdy+I8UTetycgUqXWSDwpgv193Ka+qRsmBY8Q= +gotest.tools/v3 v3.5.2/go.mod h1:LtdLGcnqToBH83WByAAi/wiwSFCArdFIUV/xxN4pcjA= +nullprogram.com/x/optparse v1.0.0/go.mod h1:KdyPE+Igbe0jQUrVfMqDMeJQIJZEuyV7pjYmp6pbG50= diff --git a/persys-forgery/internal/build/docker.go b/persys-forgery/internal/build/docker.go new file mode 100644 index 0000000..20187bc --- /dev/null +++ b/persys-forgery/internal/build/docker.go @@ -0,0 +1,187 @@ +package build + +import ( + "context" + "encoding/base64" + "encoding/json" + "fmt" + "os" + "os/exec" + "path/filepath" + + "archive/tar" + "bytes" + "io" + + "github.com/docker/docker/api/types" + "github.com/docker/docker/api/types/container" + "github.com/docker/docker/api/types/image" + "github.com/docker/docker/api/types/registry" + "github.com/docker/docker/api/types/strslice" + "github.com/docker/docker/client" + "github.com/persys-dev/persys-cloud/persys-forgery/internal/models" +) + +type Docker struct{} + +func (d *Docker) Build(ctx context.Context, req models.BuildRequest) error { + // Get workspace from metadata (set by orchestrator) + workspace := "/tmp/forge-builds" // default fallback + if req.Metadata != nil { + if ws, ok := req.Metadata["workspace"].(string); ok { + workspace = ws + } + } + + // 1. Clone or pull repo + repoURL := req.Source + if _, err := os.Stat(filepath.Join(workspace, ".git")); os.IsNotExist(err) { + cmd := exec.CommandContext(ctx, "git", "clone", repoURL, workspace) + if out, err := cmd.CombinedOutput(); err != nil { + return fmt.Errorf("git clone failed: %v, output: %s", err, string(out)) + } + } else { + cmd := exec.CommandContext(ctx, "git", "pull") + cmd.Dir = workspace + if out, err := cmd.CombinedOutput(); err != nil { + return fmt.Errorf("git pull failed: %v, output: %s", err, string(out)) + } + } + + // 2. Checkout commit if specified + if req.CommitHash != "" { + cmd := exec.CommandContext(ctx, "git", "checkout", req.CommitHash) + cmd.Dir = workspace + if out, err := cmd.CombinedOutput(); err != nil { + return fmt.Errorf("git checkout failed: %v, output: %s", err, string(out)) + } + } + + // 3. Build/Run + switch req.Type { + case models.BuildTypeCompose: + cmd := exec.CommandContext(ctx, "docker-compose", "up", "--build", "-d") + cmd.Dir = workspace + if out, err := cmd.CombinedOutput(); err != nil { + return fmt.Errorf("docker-compose up failed: %v, output: %s", err, string(out)) + } + case models.BuildTypeDockerfile: + cli, err := client.NewClientWithOpts(client.FromEnv, client.WithAPIVersionNegotiation()) + if err != nil { + return fmt.Errorf("failed to create Docker client: %w", err) + } + // Create tar archive of workspace as build context + buf := new(bytes.Buffer) + tarWriter := tar.NewWriter(buf) + err = filepath.Walk(workspace, func(path string, info os.FileInfo, err error) error { + if err != nil { + return err + } + relPath, err := filepath.Rel(workspace, path) + if err != nil { + return err + } + hdr, err := tar.FileInfoHeader(info, "") + if err != nil { + return err + } + hdr.Name = relPath + if err := tarWriter.WriteHeader(hdr); err != nil { + return err + } + if !info.IsDir() { + f, err := os.Open(path) + if err != nil { + return err + } + defer f.Close() + if _, err := io.Copy(tarWriter, f); err != nil { + return err + } + } + return nil + }) + tarWriter.Close() + if err != nil { + return fmt.Errorf("failed to create build context: %w", err) + } + imageName := req.ProjectName + buildResp, err := cli.ImageBuild(ctx, buf, types.ImageBuildOptions{ + Tags: []string{imageName}, + Remove: true, + Dockerfile: "Dockerfile", + }) + if err != nil { + return fmt.Errorf("docker build failed: %w", err) + } + io.Copy(io.Discard, buildResp.Body) + buildResp.Body.Close() + + // Only push if PushArtifact is true + if req.PushArtifact { + // Tag for Nexus + nexusRepo := os.Getenv("FORGE_NEXUS_DOCKER_REPO") + if req.NexusRepo != "" { + nexusRepo = req.NexusRepo + } + if nexusRepo != "" { + imageTag := fmt.Sprintf("%s/%s:latest", nexusRepo, req.ProjectName) + if err := cli.ImageTag(ctx, imageName, imageTag); err != nil { + return fmt.Errorf("docker tag failed: %w", err) + } + // Login to Nexus + nexusUser := os.Getenv("FORGE_NEXUS_USER") + nexusPass := os.Getenv("FORGE_NEXUS_PASSWORD") + _, err = cli.RegistryLogin(ctx, registry.AuthConfig{ + Username: nexusUser, + Password: nexusPass, + ServerAddress: nexusRepo, + }) + if err != nil { + return fmt.Errorf("docker login failed: %w", err) + } + // Push image + authConfig := registry.AuthConfig{ + Username: nexusUser, + Password: nexusPass, + ServerAddress: nexusRepo, + } + authBytes, _ := json.Marshal(authConfig) + encodedAuth := base64.URLEncoding.EncodeToString(authBytes) + pushResp, err := cli.ImagePush(ctx, imageTag, image.PushOptions{ + RegistryAuth: encodedAuth, + }) + if err != nil { + return fmt.Errorf("docker push failed: %w", err) + } + io.Copy(io.Discard, pushResp) + pushResp.Close() + } + } + + // Run container if specified + if req.Metadata != nil { + if runContainer, ok := req.Metadata["run_container"].(bool); ok && runContainer { + imageToRun := imageName + if req.NexusRepo != "" { + imageToRun = fmt.Sprintf("%s/%s:latest", req.NexusRepo, req.ProjectName) + } + _, err = cli.ContainerCreate(ctx, &container.Config{ + Image: imageToRun, + Cmd: strslice.StrSlice{}, + }, nil, nil, nil, req.ProjectName) + if err != nil { + return fmt.Errorf("docker run failed: %w", err) + } + if err := cli.ContainerStart(ctx, req.ProjectName, container.StartOptions{}); err != nil { + return fmt.Errorf("docker start failed: %w", err) + } + } + } + case models.BuildTypePipeline: + return fmt.Errorf("pipeline type not supported in Docker client") + default: + return fmt.Errorf("unknown build type: %s", req.Type) + } + return nil +} diff --git a/persys-forgery/internal/build/orchestrator.go b/persys-forgery/internal/build/orchestrator.go new file mode 100644 index 0000000..c283fe5 --- /dev/null +++ b/persys-forgery/internal/build/orchestrator.go @@ -0,0 +1,196 @@ +package build + +import ( + "context" + "errors" + "fmt" + "log" + "os" + "path/filepath" + "time" + + "github.com/persys-dev/persys-cloud/persys-forgery/internal/models" + "github.com/persys-dev/persys-cloud/persys-forgery/internal/operator" +) + +type OperatorClient interface { + CreateBuildCRD(ctx context.Context, crd operator.BuildCRD) error +} + +type Orchestrator struct { + Docker DockerClient + OperatorClient OperatorClient + WorkspaceDir string +} + +type DockerClient interface { + Build(ctx context.Context, req models.BuildRequest) error +} + +// NewOrchestrator creates a new orchestrator with initialized clients +func NewOrchestrator(workspaceDir string) *Orchestrator { + if workspaceDir == "" { + workspaceDir = "/tmp/forge-builds" + } + + return &Orchestrator{ + WorkspaceDir: workspaceDir, + } +} + +// SetDockerClient sets the Docker client for the orchestrator +func (o *Orchestrator) SetDockerClient(client DockerClient) { + o.Docker = client +} + +// SetOperatorClient sets the operator client for the orchestrator +func (o *Orchestrator) SetOperatorClient(client OperatorClient) { + o.OperatorClient = client +} + +// BuildWithStrategy executes a build using the specified strategy +func (o *Orchestrator) BuildWithStrategy(ctx context.Context, req models.BuildRequest, strategy string) error { + // Use strategy from request if not explicitly provided + if strategy == "" { + strategy = req.Strategy + } + + // Validate required fields + if err := o.validateBuildRequest(req); err != nil { + return fmt.Errorf("invalid build request: %w", err) + } + + // Create workspace for this build + workspace := o.createWorkspace(req) + defer o.cleanupWorkspace(workspace) + + log.Printf("Starting build for project %s with strategy %s", req.ProjectName, strategy) + + // Execute build based on strategy + switch strategy { + case "local": + return o.executeLocalBuild(ctx, req, workspace) + case "operator": + return o.executeOperatorBuild(ctx, req) + case "prow": + return o.executeProwBuild(ctx, req) + default: + return fmt.Errorf("unknown build strategy: %s", strategy) + } +} + +// validateBuildRequest validates the build request +func (o *Orchestrator) validateBuildRequest(req models.BuildRequest) error { + if req.ProjectName == "" { + return errors.New("project name is required") + } + if req.Type == "" { + return errors.New("build type is required") + } + if req.Source == "" { + return errors.New("source repository is required") + } + if req.CommitHash == "" { + return errors.New("commit hash is required") + } + if req.Strategy == "" { + return errors.New("build strategy is required") + } + + // Validate build type + validTypes := map[string]bool{ + models.BuildTypeDockerfile: true, + models.BuildTypeCompose: true, + models.BuildTypePipeline: true, + } + if !validTypes[req.Type] { + return fmt.Errorf("invalid build type: %s", req.Type) + } + + return nil +} + +// createWorkspace creates a workspace directory for the build +func (o *Orchestrator) createWorkspace(req models.BuildRequest) string { + timestamp := time.Now().UnixNano() + workspace := filepath.Join(o.WorkspaceDir, fmt.Sprintf("%s-%d", req.ProjectName, timestamp)) + + if err := os.MkdirAll(workspace, 0755); err != nil { + log.Printf("Warning: failed to create workspace %s: %v", workspace, err) + } + + return workspace +} + +// cleanupWorkspace cleans up the workspace directory +func (o *Orchestrator) cleanupWorkspace(workspace string) { + if err := os.RemoveAll(workspace); err != nil { + log.Printf("Warning: failed to cleanup workspace %s: %v", workspace, err) + } +} + +// executeLocalBuild executes a local build using Docker +func (o *Orchestrator) executeLocalBuild(ctx context.Context, req models.BuildRequest, workspace string) error { + if o.Docker == nil { + return errors.New("Docker client not initialized") + } + + log.Printf("Executing local build for %s in workspace %s", req.ProjectName, workspace) + + // Initialize metadata if nil + if req.Metadata == nil { + req.Metadata = make(map[string]interface{}) + } + + // Update request with workspace path + req.Metadata["workspace"] = workspace + + return o.Docker.Build(ctx, req) +} + +// executeOperatorBuild executes a build using Kubernetes operator +func (o *Orchestrator) executeOperatorBuild(ctx context.Context, req models.BuildRequest) error { + if o.OperatorClient == nil { + return errors.New("operator client not initialized") + } + + log.Printf("Executing operator build for %s", req.ProjectName) + + // Create BuildCRD for the operator + crd := operator.BuildCRD{ + ProjectName: req.ProjectName, + Image: o.generateImageName(req), + Source: req.Source, + CommitHash: req.CommitHash, + Pipeline: req.Pipeline, + } + + return o.OperatorClient.CreateBuildCRD(ctx, crd) +} + +// executeProwBuild executes a build using Prow (placeholder for future implementation) +func (o *Orchestrator) executeProwBuild(ctx context.Context, req models.BuildRequest) error { + log.Printf("Executing Prow build for %s", req.ProjectName) + + // TODO: Implement Prow build strategy + // This would involve creating a Prow job configuration + // and submitting it to the Prow cluster + + return fmt.Errorf("prow build strategy not yet implemented") +} + +// generateImageName generates an image name for the build +func (o *Orchestrator) generateImageName(req models.BuildRequest) string { + // Use Nexus repo if specified, otherwise use project name + if req.NexusRepo != "" { + return fmt.Sprintf("%s/%s:latest", req.NexusRepo, req.ProjectName) + } + return fmt.Sprintf("%s:latest", req.ProjectName) +} + +// GetBuildStatus returns the status of a build (placeholder for future implementation) +func (o *Orchestrator) GetBuildStatus(ctx context.Context, buildID string) (string, error) { + // TODO: Implement build status tracking + // This would involve querying the build status from the appropriate backend + return "unknown", fmt.Errorf("build status tracking not yet implemented") +} diff --git a/persys-forgery/internal/db/mysql.go b/persys-forgery/internal/db/mysql.go new file mode 100644 index 0000000..187d55b --- /dev/null +++ b/persys-forgery/internal/db/mysql.go @@ -0,0 +1,53 @@ +package db + +import ( + "log" + + "gorm.io/driver/mysql" + "gorm.io/gorm" + + "github.com/persys-dev/persys-cloud/persys-forgery/internal/models" +) + +var DB *gorm.DB + +func InitMySQL(dsn string) error { + db, err := gorm.Open(mysql.Open(dsn), &gorm.Config{}) + if err != nil { + return err + } + if err := db.AutoMigrate(&models.Project{}, &models.Job{}, &models.ProjectSecret{}); err != nil { + return err + } + DB = db + log.Println("MySQL connected and migrated.") + return nil +} + +func CreateProject(db *gorm.DB, p *models.Project) error { + return db.Create(p).Error +} + +func GetProject(db *gorm.DB, name string) (*models.Project, error) { + var p models.Project + if err := db.Where("name = ?", name).First(&p).Error; err != nil { + return nil, err + } + return &p, nil +} + +func ListProjects(db *gorm.DB) ([]models.Project, error) { + var projects []models.Project + if err := db.Find(&projects).Error; err != nil { + return nil, err + } + return projects, nil +} + +func DeleteProject(db *gorm.DB, name string) error { + return db.Where("name = ?", name).Delete(&models.Project{}).Error +} + +func UpdateProject(db *gorm.DB, name string, updates map[string]interface{}) error { + return db.Model(&models.Project{}).Where("name = ?", name).Updates(updates).Error +} diff --git a/persys-forgery/internal/handlers/build.go b/persys-forgery/internal/handlers/build.go new file mode 100644 index 0000000..7a96bd7 --- /dev/null +++ b/persys-forgery/internal/handlers/build.go @@ -0,0 +1,67 @@ +package handlers + +import ( + "context" + "net/http" + "strconv" + + "github.com/gin-gonic/gin" + "github.com/persys-dev/persys-cloud/persys-forgery/internal/build" + "github.com/persys-dev/persys-cloud/persys-forgery/internal/db" + "github.com/persys-dev/persys-cloud/persys-forgery/internal/models" +) + +type BuildHandler struct { + Orchestrator *build.Orchestrator +} + +func NewBuildHandler(orchestrator *build.Orchestrator) *BuildHandler { + return &BuildHandler{Orchestrator: orchestrator} +} + +// POST /build +func (h *BuildHandler) Build(c *gin.Context) { + var req models.BuildRequest + if err := c.ShouldBindJSON(&req); err != nil { + c.JSON(http.StatusBadRequest, gin.H{"error": "invalid request"}) + return + } + // Required field validation + if req.ProjectName == "" || req.Type == "" || req.Source == "" || req.Strategy == "" { + c.JSON(http.StatusBadRequest, gin.H{"error": "missing required fields"}) + return + } + ctx := context.Background() + job := models.Job{ + ProjectID: 0, // TODO: look up project by name + Type: req.Type, + Status: "pending", + CommitHash: req.CommitHash, + } + db.DB.Create(&job) + go func(jobID uint) { + err := h.Orchestrator.BuildWithStrategy(ctx, req, req.Strategy) + status := "success" + if err != nil { + status = "failed" + } + db.DB.Model(&models.Job{}).Where("id = ?", jobID).Update("status", status) + }(job.ID) + c.JSON(http.StatusAccepted, gin.H{"job_id": job.ID, "status": "pending"}) +} + +// GET /status/:job_id +func (h *BuildHandler) Status(c *gin.Context) { + idStr := c.Param("job_id") + id, err := strconv.Atoi(idStr) + if err != nil { + c.JSON(http.StatusBadRequest, gin.H{"error": "invalid job id"}) + return + } + var job models.Job + if err := db.DB.First(&job, id).Error; err != nil { + c.JSON(http.StatusNotFound, gin.H{"error": "job not found"}) + return + } + c.JSON(http.StatusOK, job) +} diff --git a/persys-forgery/internal/handlers/project.go b/persys-forgery/internal/handlers/project.go new file mode 100644 index 0000000..7e4625e --- /dev/null +++ b/persys-forgery/internal/handlers/project.go @@ -0,0 +1,82 @@ +package handlers + +import ( + "net/http" + "strings" + + "github.com/gin-gonic/gin" + "github.com/persys-dev/persys-cloud/persys-forgery/internal/models" + "gorm.io/gorm" +) + +type ProjectHandler struct { + DB *gorm.DB +} + +func NewProjectHandler(db *gorm.DB) *ProjectHandler { + return &ProjectHandler{DB: db} +} + +// POST /projects +func (h *ProjectHandler) CreateProject(c *gin.Context) { + var req models.Project + if err := c.ShouldBindJSON(&req); err != nil { + c.JSON(http.StatusBadRequest, gin.H{"error": "invalid request"}) + return + } + if err := h.DB.Create(&req).Error; err != nil { + if strings.Contains(err.Error(), "UNIQUE constraint failed") { + c.JSON(http.StatusConflict, gin.H{"error": "project already exists"}) + return + } + c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()}) + return + } + c.JSON(http.StatusCreated, req) +} + +// GET /projects +func (h *ProjectHandler) ListProjects(c *gin.Context) { + var projects []models.Project + if err := h.DB.Find(&projects).Error; err != nil { + c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()}) + return + } + c.JSON(http.StatusOK, projects) +} + +// GET /projects/:project +func (h *ProjectHandler) GetProject(c *gin.Context) { + name := c.Param("project") + var project models.Project + if err := h.DB.Where("name = ?", name).First(&project).Error; err != nil { + c.JSON(http.StatusNotFound, gin.H{"error": "project not found"}) + return + } + c.JSON(http.StatusOK, project) +} + +// PUT /projects/:project +func (h *ProjectHandler) UpdateProject(c *gin.Context) { + name := c.Param("project") + var updates map[string]interface{} + if err := c.ShouldBindJSON(&updates); err != nil { + c.JSON(http.StatusBadRequest, gin.H{"error": "invalid request"}) + return + } + if err := h.DB.Model(&models.Project{}).Where("name = ?", name).Updates(updates).Error; err != nil { + c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()}) + return + } + c.JSON(http.StatusOK, gin.H{"status": "updated"}) +} + +// DELETE /projects/:project +func (h *ProjectHandler) DeleteProject(c *gin.Context) { + name := c.Param("project") + if err := h.DB.Where("name = ?", name).Delete(&models.Project{}).Error; err != nil { + c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()}) + return + } + c.JSON(http.StatusOK, gin.H{"status": "deleted"}) +} diff --git a/persys-forgery/internal/handlers/queue.go b/persys-forgery/internal/handlers/queue.go new file mode 100644 index 0000000..139b840 --- /dev/null +++ b/persys-forgery/internal/handlers/queue.go @@ -0,0 +1,13 @@ +package handlers + +import ( + "net/http" + + "github.com/gin-gonic/gin" +) + +// GET /queue +func (h *BuildHandler) Queue(c *gin.Context) { + // TODO: Implement queue inspection logic + c.JSON(http.StatusNotImplemented, gin.H{"error": "not implemented"}) +} diff --git a/persys-forgery/internal/handlers/secret.go b/persys-forgery/internal/handlers/secret.go new file mode 100644 index 0000000..0e2d590 --- /dev/null +++ b/persys-forgery/internal/handlers/secret.go @@ -0,0 +1,85 @@ +package handlers + +import ( + "net/http" + + "github.com/gin-gonic/gin" + "github.com/persys-dev/persys-cloud/persys-forgery/internal/secrets" +) + +type SecretHandler struct { + Manager *secrets.Manager +} + +func NewSecretHandler(manager *secrets.Manager) *SecretHandler { + return &SecretHandler{Manager: manager} +} + +// POST /secrets +func (h *SecretHandler) SetSecret(c *gin.Context) { + var req struct { + Key string `json:"key"` + Value string `json:"value"` + } + if err := c.ShouldBindJSON(&req); err != nil { + c.JSON(http.StatusBadRequest, gin.H{"error": "invalid request"}) + return + } + if err := h.Manager.Set("global", req.Key, req.Value); err != nil { + c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()}) + return + } + c.JSON(http.StatusOK, gin.H{"key": req.Key, "status": "ok"}) +} + +// GET /secrets/:key +func (h *SecretHandler) GetSecret(c *gin.Context) { + key := c.Param("key") + val, err := h.Manager.Get("global", key) + if err != nil { + c.JSON(http.StatusNotFound, gin.H{"error": err.Error()}) + return + } + c.JSON(http.StatusOK, gin.H{"key": key, "value": val}) +} + +// POST /secrets/:project +func (h *SecretHandler) SetProjectSecret(c *gin.Context) { + project := c.Param("project") + var req struct { + Key string `json:"key"` + Value string `json:"value"` + } + if err := c.ShouldBindJSON(&req); err != nil { + c.JSON(http.StatusBadRequest, gin.H{"error": "invalid request"}) + return + } + if err := h.Manager.Set(project, req.Key, req.Value); err != nil { + c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()}) + return + } + c.JSON(http.StatusOK, gin.H{"status": "ok"}) +} + +// GET /secrets/:project/:key +func (h *SecretHandler) GetProjectSecret(c *gin.Context) { + project := c.Param("project") + key := c.Param("key") + val, err := h.Manager.Get(project, key) + if err != nil { + c.JSON(http.StatusNotFound, gin.H{"error": err.Error()}) + return + } + c.JSON(http.StatusOK, gin.H{"key": key, "value": val}) +} + +// GET /secrets/:project +func (h *SecretHandler) ListProjectSecrets(c *gin.Context) { + project := c.Param("project") + keys, err := h.Manager.List(project) + if err != nil { + c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()}) + return + } + c.JSON(http.StatusOK, gin.H{"keys": keys}) +} diff --git a/persys-forgery/internal/models/build.go b/persys-forgery/internal/models/build.go new file mode 100644 index 0000000..0f5b986 --- /dev/null +++ b/persys-forgery/internal/models/build.go @@ -0,0 +1,39 @@ +package models + +import "time" + +type BuildRequest struct { + ID string `json:"id,omitempty"` + ProjectName string `json:"project_name"` + Type string `json:"type"` + Source string `json:"source"` + CommitHash string `json:"commit_hash"` + Branch string `json:"branch,omitempty"` + Pipeline string `json:"pipeline,omitempty"` + Strategy string `json:"strategy"` // local, prow, operator + PushArtifact bool `json:"push_artifact,omitempty"` + NexusRepo string `json:"nexus_repo,omitempty"` + WebhookData map[string]interface{} `json:"webhook_data,omitempty"` + Metadata map[string]interface{} `json:"metadata,omitempty"` + CreatedAt time.Time `json:"created_at,omitempty"` +} + +type WebhookData struct { + EventType string `json:"event_type"` // push, pull_request, tag + Repository string `json:"repository"` // org/repo + Sender string `json:"sender"` // username + Ref string `json:"ref"` // refs/heads/main + Before string `json:"before"` // old commit + After string `json:"after"` // new commit + PullRequest *struct { + Number int `json:"number"` + Title string `json:"title"` + State string `json:"state"` + } `json:"pull_request,omitempty"` +} + +const ( + BuildTypeCompose = "compose" + BuildTypeDockerfile = "dockerfile" + BuildTypePipeline = "pipeline" +) diff --git a/persys-forgery/internal/models/job.go b/persys-forgery/internal/models/job.go new file mode 100644 index 0000000..9c637cd --- /dev/null +++ b/persys-forgery/internal/models/job.go @@ -0,0 +1,12 @@ +package models + +type Job struct { + ID uint `gorm:"primaryKey"` + ProjectID uint `gorm:"index"` + Type string // build, deploy, etc. + Status string // pending, running, success, failed + CommitHash string + Log string `gorm:"type:text"` + CreatedAt int64 + UpdatedAt int64 +} diff --git a/persys-forgery/internal/models/project.go b/persys-forgery/internal/models/project.go new file mode 100644 index 0000000..1b4d534 --- /dev/null +++ b/persys-forgery/internal/models/project.go @@ -0,0 +1,12 @@ +package models + +import "time" + +type Project struct { + ID uint `gorm:"primaryKey"` + Name string `gorm:"uniqueIndex;not null"` + RepoURL string `gorm:"not null"` + DefaultBranch string `gorm:"default:'main'"` + CreatedAt time.Time + UpdatedAt time.Time +} diff --git a/persys-forgery/internal/models/secret.go b/persys-forgery/internal/models/secret.go new file mode 100644 index 0000000..cef7622 --- /dev/null +++ b/persys-forgery/internal/models/secret.go @@ -0,0 +1,8 @@ +package models + +type ProjectSecret struct { + ID uint `gorm:"primaryKey"` + Project string `gorm:"index;not null"` + Key string `gorm:"not null"` + Value string `gorm:"not null"` +} diff --git a/persys-forgery/internal/operator/client.go b/persys-forgery/internal/operator/client.go new file mode 100644 index 0000000..bd30fa8 --- /dev/null +++ b/persys-forgery/internal/operator/client.go @@ -0,0 +1,24 @@ +package operator + +import ( + "context" + "fmt" +) + +type Client struct { + // TODO: Add k8s client-go fields +} + +type BuildCRD struct { + ProjectName string + Image string + Source string + CommitHash string + Pipeline string +} + +func (c *Client) CreateBuildCRD(ctx context.Context, crd BuildCRD) error { + // TODO: Use client-go to create the CRD in the cluster + fmt.Printf("[stub] Creating Build CRD: %+v\n", crd) + return nil +} diff --git a/persys-forgery/internal/pipeline/pipeline.go b/persys-forgery/internal/pipeline/pipeline.go new file mode 100644 index 0000000..368c915 --- /dev/null +++ b/persys-forgery/internal/pipeline/pipeline.go @@ -0,0 +1,55 @@ +package pipeline + +import ( + "context" + "fmt" + "os/exec" + + "github.com/persys-dev/persys-cloud/persys-forgery/internal/models" + "gopkg.in/yaml.v3" +) + +type Runner struct{} + +type Pipeline struct { + Steps []PipelineStep `yaml:"steps"` +} + +type PipelineStep struct { + Name string `yaml:"name"` + Type string `yaml:"type"` + Script string `yaml:"script"` +} + +func (r *Runner) Run(ctx context.Context, pipelineYAML string) error { + var pipeline Pipeline + if err := yaml.Unmarshal([]byte(pipelineYAML), &pipeline); err != nil { + return fmt.Errorf("failed to parse pipeline yaml: %w", err) + } + for _, step := range pipeline.Steps { + cmd := exec.CommandContext(ctx, "/bin/sh", "-c", step.Script) + out, err := cmd.CombinedOutput() + if err != nil { + return fmt.Errorf("pipeline step '%s' failed: %v, output: %s", step.Name, err, string(out)) + } + } + return nil +} + +// DockerClient is an interface for running Docker builds +// This avoids import cycles +type DockerClient interface { + Build(ctx context.Context, req models.BuildRequest) error +} + +func RunPipeline(ctx context.Context, req models.BuildRequest, docker DockerClient) error { + if req.Pipeline != "" { + r := &Runner{} + if err := r.Run(ctx, req.Pipeline); err == nil { + return nil + } + // If pipeline fails, fallback + } + // Fallback to Docker Compose or Dockerfile + return docker.Build(ctx, req) +} diff --git a/persys-forgery/internal/queue/worker.go b/persys-forgery/internal/queue/worker.go new file mode 100644 index 0000000..3afc8dc --- /dev/null +++ b/persys-forgery/internal/queue/worker.go @@ -0,0 +1,41 @@ +package queue + +import ( + "context" + "encoding/json" + "log" + + "github.com/persys-dev/persys-cloud/persys-forgery/internal/build" + "github.com/persys-dev/persys-cloud/persys-forgery/internal/models" + "github.com/persys-dev/persys-cloud/persys-forgery/utils" + "github.com/redis/go-redis/v9" +) + +func StartRedisWorker(cfg *utils.Config, orchestrator *build.Orchestrator) { + rdb := redis.NewClient(&redis.Options{ + Addr: cfg.Redis.Addr, + Password: cfg.Redis.Password, + DB: cfg.Redis.DB, + }) + ctx := context.Background() + for { + res, err := rdb.BLPop(ctx, 0, "forge:builds").Result() + if err != nil { + log.Println("Redis error:", err) + continue + } + if len(res) < 2 { + continue + } + var req models.BuildRequest + if err := json.Unmarshal([]byte(res[1]), &req); err != nil { + log.Println("Failed to unmarshal build request:", err) + continue + } + log.Printf("Dequeued build job: %+v", req) + go func(r models.BuildRequest) { + ctx := context.Background() + _ = orchestrator.BuildWithStrategy(ctx, r, r.Strategy) + }(req) + } +} diff --git a/persys-forgery/internal/secrets/secrets.go b/persys-forgery/internal/secrets/secrets.go new file mode 100644 index 0000000..9313346 --- /dev/null +++ b/persys-forgery/internal/secrets/secrets.go @@ -0,0 +1,56 @@ +package secrets + +import ( + "github.com/persys-dev/persys-cloud/persys-forgery/internal/models" + "gorm.io/gorm" +) + +type Manager struct { + DB *gorm.DB +} + +func (m *Manager) Set(project, key, value string) error { + return SetProjectSecret(m.DB, project, key, value) +} + +func (m *Manager) Get(project, key string) (string, error) { + return GetProjectSecret(m.DB, project, key) +} + +func (m *Manager) List(project string) ([]string, error) { + return ListProjectSecrets(m.DB, project) +} + +func MigrateSecrets(db *gorm.DB) error { + return db.AutoMigrate(&models.ProjectSecret{}) +} + +func SetProjectSecret(db *gorm.DB, project, key, value string) error { + var s models.ProjectSecret + if err := db.Where("project = ? AND key = ?", project, key).First(&s).Error; err == nil { + s.Value = value + return db.Save(&s).Error + } + s = models.ProjectSecret{Project: project, Key: key, Value: value} + return db.Create(&s).Error +} + +func GetProjectSecret(db *gorm.DB, project, key string) (string, error) { + var s models.ProjectSecret + if err := db.Where("project = ? AND key = ?", project, key).First(&s).Error; err != nil { + return "", err + } + return s.Value, nil +} + +func ListProjectSecrets(db *gorm.DB, project string) ([]string, error) { + var secrets []models.ProjectSecret + if err := db.Where("project = ?", project).Find(&secrets).Error; err != nil { + return nil, err + } + keys := make([]string, len(secrets)) + for i, s := range secrets { + keys[i] = s.Key + } + return keys, nil +} diff --git a/persys-forgery/tests/persys_forge_test.go b/persys-forgery/tests/persys_forge_test.go new file mode 100644 index 0000000..3080c46 --- /dev/null +++ b/persys-forgery/tests/persys_forge_test.go @@ -0,0 +1,195 @@ +package tests + +import ( + "bytes" + "context" + "encoding/json" + "net/http" + "net/http/httptest" + "testing" + + "gorm.io/driver/sqlite" + "gorm.io/gorm" + + "github.com/gin-gonic/gin" + "github.com/persys-dev/persys-cloud/persys-forgery/internal/build" + "github.com/persys-dev/persys-cloud/persys-forgery/internal/db" + "github.com/persys-dev/persys-cloud/persys-forgery/internal/handlers" + "github.com/persys-dev/persys-cloud/persys-forgery/internal/models" + "github.com/persys-dev/persys-cloud/persys-forgery/internal/secrets" +) + +type mockOrchestrator struct{} + +func (m *mockOrchestrator) BuildWithStrategy(ctx interface{}, req models.BuildRequest, strategy string) error { + return nil +} + +type mockSecretManager struct{} + +func (m *mockSecretManager) Set(project, key, value string) error { return nil } +func (m *mockSecretManager) Get(project, key string) (string, error) { return "mocked", nil } +func (m *mockSecretManager) List(project string) ([]string, error) { + return []string{"key1", "key2"}, nil +} + +type mockDocker struct{} + +func (m *mockDocker) Build(ctx context.Context, req models.BuildRequest) error { return nil } + +func init() { + testDB, _ := gorm.Open(sqlite.Open(":memory:"), &gorm.Config{}) + db.DB = testDB + // Migrate your models + testDB.AutoMigrate(&models.Project{}, &models.Job{}, &models.ProjectSecret{}) +} + +func setupRouter(testDB *gorm.DB) *gin.Engine { + gin.SetMode(gin.TestMode) + r := gin.Default() + + // Use the new orchestrator constructor + orchestrator := build.NewOrchestrator("/tmp/test-forge-builds") + orchestrator.SetDockerClient(&mockDocker{}) + + buildHandler := handlers.NewBuildHandler(orchestrator) + projectHandler := handlers.NewProjectHandler(testDB) + secretHandler := handlers.NewSecretHandler(&secrets.Manager{DB: testDB}) + + r.POST("/build", buildHandler.Build) + r.GET("/status/:job_id", buildHandler.Status) + r.POST("/projects", projectHandler.CreateProject) + r.GET("/projects", projectHandler.ListProjects) + r.GET("/projects/:project", projectHandler.GetProject) + r.PUT("/projects/:project", projectHandler.UpdateProject) + r.DELETE("/projects/:project", projectHandler.DeleteProject) + r.POST("/secrets", secretHandler.SetSecret) + // Use prefix for project secrets to avoid route conflict + r.GET("/secrets/project/:project/:key", secretHandler.GetProjectSecret) + r.GET("/secrets/project/:project", secretHandler.ListProjectSecrets) + r.GET("/secrets/:key", secretHandler.GetSecret) + return r +} + +// Add a struct and a slice to collect test results +type testResult struct { + label string + got int + want int + pass bool +} + +var testResults []testResult + +func assertWithEmoji(t *testing.T, got, want int, label string) { + pass := got == want + if pass { + t.Logf("✅ %s: got %d as expected", label, got) + } else { + t.Errorf("❌ %s: got %d, want %d", label, got, want) + } + testResults = append(testResults, testResult{label, got, want, pass}) +} + +func TestPersysForge_AllEndpoints(t *testing.T) { + testResults = nil // reset before running + testDB, _ := gorm.Open(sqlite.Open(":memory:"), &gorm.Config{}) + testDB.AutoMigrate(&models.Project{}, &models.Job{}, &models.ProjectSecret{}) + r := setupRouter(testDB) + + // Test build (valid) + buildPayload := models.BuildRequest{ + ProjectName: "test-proj", + Type: "dockerfile", + Source: "https://github.com/org/repo.git", + CommitHash: "abc123", + Strategy: "local", + Metadata: make(map[string]interface{}), + } + body, _ := json.Marshal(buildPayload) + w := httptest.NewRecorder() + req, _ := http.NewRequest("POST", "/build", bytes.NewBuffer(body)) + req.Header.Set("Content-Type", "application/json") + r.ServeHTTP(w, req) + assertWithEmoji(t, w.Code, http.StatusAccepted, "Build (valid)") + + // Test build (invalid payload) + w = httptest.NewRecorder() + req, _ = http.NewRequest("POST", "/build", bytes.NewBuffer([]byte(`{"foo":123}`))) + req.Header.Set("Content-Type", "application/json") + r.ServeHTTP(w, req) + assertWithEmoji(t, w.Code, http.StatusBadRequest, "Build (invalid payload)") + + // Test project create (valid) + projPayload := models.Project{Name: "test-proj", RepoURL: "https://github.com/org/repo.git"} + body, _ = json.Marshal(projPayload) + w = httptest.NewRecorder() + req, _ = http.NewRequest("POST", "/projects", bytes.NewBuffer(body)) + req.Header.Set("Content-Type", "application/json") + r.ServeHTTP(w, req) + assertWithEmoji(t, w.Code, http.StatusCreated, "Project create (valid)") + + // Test project create (duplicate) + w = httptest.NewRecorder() + req, _ = http.NewRequest("POST", "/projects", bytes.NewBuffer(body)) + req.Header.Set("Content-Type", "application/json") + r.ServeHTTP(w, req) + assertWithEmoji(t, w.Code, http.StatusConflict, "Project create (duplicate)") + + // Test get non-existent project + w = httptest.NewRecorder() + req, _ = http.NewRequest("GET", "/projects/doesnotexist", nil) + r.ServeHTTP(w, req) + assertWithEmoji(t, w.Code, http.StatusNotFound, "Get non-existent project") + + // Test update project (invalid payload) + w = httptest.NewRecorder() + req, _ = http.NewRequest("PUT", "/projects/test-proj", bytes.NewBuffer([]byte(`{"bad":}`))) + req.Header.Set("Content-Type", "application/json") + r.ServeHTTP(w, req) + assertWithEmoji(t, w.Code, http.StatusBadRequest, "Update project (invalid payload)") + + // Test delete non-existent project + w = httptest.NewRecorder() + req, _ = http.NewRequest("DELETE", "/projects/doesnotexist", nil) + r.ServeHTTP(w, req) + assertWithEmoji(t, w.Code, http.StatusOK, "Delete non-existent project") + + // Test secret set (valid) + secretPayload := map[string]string{"key": "foo", "value": "bar"} + body, _ = json.Marshal(secretPayload) + w = httptest.NewRecorder() + req, _ = http.NewRequest("POST", "/secrets", bytes.NewBuffer(body)) + req.Header.Set("Content-Type", "application/json") + r.ServeHTTP(w, req) + assertWithEmoji(t, w.Code, http.StatusOK, "Secret set (valid)") + + // Test secret set (invalid payload) + w = httptest.NewRecorder() + req, _ = http.NewRequest("POST", "/secrets", bytes.NewBuffer([]byte(`{"bad":}`))) + req.Header.Set("Content-Type", "application/json") + r.ServeHTTP(w, req) + assertWithEmoji(t, w.Code, http.StatusBadRequest, "Secret set (invalid payload)") + + // Test get non-existent secret + w = httptest.NewRecorder() + req, _ = http.NewRequest("GET", "/secrets/doesnotexist", nil) + r.ServeHTTP(w, req) + assertWithEmoji(t, w.Code, http.StatusNotFound, "Get non-existent secret") + + // Test status for non-existent job + w = httptest.NewRecorder() + req, _ = http.NewRequest("GET", "/status/9999", nil) + r.ServeHTTP(w, req) + assertWithEmoji(t, w.Code, http.StatusNotFound, "Status for non-existent job") + + // Print summary at the end + t.Logf("\nTest Summary:") + for _, res := range testResults { + if res.pass { + t.Logf("✅ %s: got %d as expected", res.label, res.got) + } else { + t.Logf("❌ %s: got %d, want %d", res.label, res.got, res.want) + } + } +} diff --git a/persys-forgery/utils/config.go b/persys-forgery/utils/config.go new file mode 100644 index 0000000..06e92c0 --- /dev/null +++ b/persys-forgery/utils/config.go @@ -0,0 +1,36 @@ +package utils + +import ( + "io/ioutil" + + "gopkg.in/yaml.v3" +) + +type RedisConfig struct { + Addr string `yaml:"addr"` + Password string `yaml:"password"` + DB int `yaml:"db"` +} + +type BuildConfig struct { + Workspace string `yaml:"workspace"` +} + +type Config struct { + MySQLDSN string `yaml:"mysql_dsn"` + Port int `yaml:"port"` + Redis RedisConfig `yaml:"redis"` + Build BuildConfig `yaml:"build"` +} + +func LoadConfig(path string) (*Config, error) { + data, err := ioutil.ReadFile(path) + if err != nil { + return nil, err + } + var cfg Config + if err := yaml.Unmarshal(data, &cfg); err != nil { + return nil, err + } + return &cfg, nil +} diff --git a/api-gateway/Dockerfile b/persys-gateway/Dockerfile similarity index 100% rename from api-gateway/Dockerfile rename to persys-gateway/Dockerfile diff --git a/api-gateway/Makefile b/persys-gateway/Makefile similarity index 100% rename from api-gateway/Makefile rename to persys-gateway/Makefile diff --git a/api-gateway/OWNERS b/persys-gateway/OWNERS similarity index 100% rename from api-gateway/OWNERS rename to persys-gateway/OWNERS diff --git a/api-gateway/README.md b/persys-gateway/README.md similarity index 100% rename from api-gateway/README.md rename to persys-gateway/README.md diff --git a/api-gateway/cmd/main.go b/persys-gateway/cmd/main.go similarity index 96% rename from api-gateway/cmd/main.go rename to persys-gateway/cmd/main.go index 7040464..fa58a38 100755 --- a/api-gateway/cmd/main.go +++ b/persys-gateway/cmd/main.go @@ -14,11 +14,11 @@ import ( "github.com/gin-contrib/cors" "github.com/gin-gonic/gin" - "github.com/persys-dev/persys-cloud/api-gateway/config" - "github.com/persys-dev/persys-cloud/api-gateway/controllers" - "github.com/persys-dev/persys-cloud/api-gateway/internal/middleware" - "github.com/persys-dev/persys-cloud/api-gateway/routes" - "github.com/persys-dev/persys-cloud/api-gateway/services" + "github.com/persys-dev/persys-cloud/persys-gateway/config" + "github.com/persys-dev/persys-cloud/persys-gateway/controllers" + "github.com/persys-dev/persys-cloud/persys-gateway/internal/middleware" + "github.com/persys-dev/persys-cloud/persys-gateway/routes" + "github.com/persys-dev/persys-cloud/persys-gateway/services" ginprometheus "github.com/zsais/go-gin-prometheus" "go.mongodb.org/mongo-driver/mongo" "go.mongodb.org/mongo-driver/mongo/options" diff --git a/api-gateway/config/config.go b/persys-gateway/config/config.go similarity index 100% rename from api-gateway/config/config.go rename to persys-gateway/config/config.go diff --git a/api-gateway/controllers/auth.controller.go b/persys-gateway/controllers/auth.controller.go similarity index 94% rename from api-gateway/controllers/auth.controller.go rename to persys-gateway/controllers/auth.controller.go index 0e65db7..d7934cb 100755 --- a/api-gateway/controllers/auth.controller.go +++ b/persys-gateway/controllers/auth.controller.go @@ -10,10 +10,10 @@ import ( "github.com/dgrijalva/jwt-go/request" "github.com/gin-gonic/gin" "github.com/google/go-github/github" - "github.com/persys-dev/persys-cloud/api-gateway/config" - "github.com/persys-dev/persys-cloud/api-gateway/models" - "github.com/persys-dev/persys-cloud/api-gateway/services" - "github.com/persys-dev/persys-cloud/api-gateway/utils" + "github.com/persys-dev/persys-cloud/persys-gateway/config" + "github.com/persys-dev/persys-cloud/persys-gateway/models" + "github.com/persys-dev/persys-cloud/persys-gateway/services" + "github.com/persys-dev/persys-cloud/persys-gateway/utils" "go.mongodb.org/mongo-driver/mongo" "golang.org/x/oauth2" oauth2gh "golang.org/x/oauth2/github" diff --git a/api-gateway/controllers/github.controller.go b/persys-gateway/controllers/github.controller.go similarity index 96% rename from api-gateway/controllers/github.controller.go rename to persys-gateway/controllers/github.controller.go index e9ecf3a..6337b5f 100644 --- a/api-gateway/controllers/github.controller.go +++ b/persys-gateway/controllers/github.controller.go @@ -8,8 +8,8 @@ import ( "github.com/gin-gonic/gin" webhook "github.com/go-playground/webhooks/v6/github" "github.com/google/go-github/github" - "github.com/persys-dev/persys-cloud/api-gateway/config" - "github.com/persys-dev/persys-cloud/api-gateway/services" + "github.com/persys-dev/persys-cloud/persys-gateway/config" + "github.com/persys-dev/persys-cloud/persys-gateway/services" "go.mongodb.org/mongo-driver/bson" "go.mongodb.org/mongo-driver/mongo" "golang.org/x/oauth2" diff --git a/api-gateway/controllers/prow.controller.go b/persys-gateway/controllers/scheduler.controller.go similarity index 98% rename from api-gateway/controllers/prow.controller.go rename to persys-gateway/controllers/scheduler.controller.go index 29a410a..d2bcabc 100644 --- a/api-gateway/controllers/prow.controller.go +++ b/persys-gateway/controllers/scheduler.controller.go @@ -6,7 +6,7 @@ import ( "net/http" "github.com/gin-gonic/gin" - "github.com/persys-dev/persys-cloud/api-gateway/services" + "github.com/persys-dev/persys-cloud/persys-gateway/services" ) type ProwController struct { diff --git a/api-gateway/go.mod b/persys-gateway/go.mod similarity index 98% rename from api-gateway/go.mod rename to persys-gateway/go.mod index cd2e84b..2c2f850 100755 --- a/api-gateway/go.mod +++ b/persys-gateway/go.mod @@ -1,4 +1,4 @@ -module github.com/persys-dev/persys-cloud/api-gateway +module github.com/persys-dev/persys-cloud/persys-gateway go 1.23.0 diff --git a/api-gateway/go.sum b/persys-gateway/go.sum similarity index 100% rename from api-gateway/go.sum rename to persys-gateway/go.sum diff --git a/api-gateway/internal/middleware/identity.go b/persys-gateway/internal/middleware/identity.go similarity index 100% rename from api-gateway/internal/middleware/identity.go rename to persys-gateway/internal/middleware/identity.go diff --git a/api-gateway/models/event.model.go b/persys-gateway/models/event.model.go similarity index 100% rename from api-gateway/models/event.model.go rename to persys-gateway/models/event.model.go diff --git a/api-gateway/models/repos.model.go b/persys-gateway/models/repos.model.go similarity index 100% rename from api-gateway/models/repos.model.go rename to persys-gateway/models/repos.model.go diff --git a/api-gateway/models/user.model.go b/persys-gateway/models/user.model.go similarity index 100% rename from api-gateway/models/user.model.go rename to persys-gateway/models/user.model.go diff --git a/api-gateway/routes/auth.routes.go b/persys-gateway/routes/auth.routes.go similarity index 93% rename from api-gateway/routes/auth.routes.go rename to persys-gateway/routes/auth.routes.go index 889ef7f..cacf84b 100755 --- a/api-gateway/routes/auth.routes.go +++ b/persys-gateway/routes/auth.routes.go @@ -2,7 +2,7 @@ package routes import ( "github.com/gin-gonic/gin" - "github.com/persys-dev/persys-cloud/api-gateway/controllers" + "github.com/persys-dev/persys-cloud/persys-gateway/controllers" ) var ( diff --git a/api-gateway/routes/github.routes.go b/persys-gateway/routes/github.routes.go similarity index 92% rename from api-gateway/routes/github.routes.go rename to persys-gateway/routes/github.routes.go index 1d75e0c..6fa82a0 100644 --- a/api-gateway/routes/github.routes.go +++ b/persys-gateway/routes/github.routes.go @@ -2,7 +2,7 @@ package routes import ( "github.com/gin-gonic/gin" - "github.com/persys-dev/persys-cloud/api-gateway/controllers" + "github.com/persys-dev/persys-cloud/persys-gateway/controllers" ) type GithubRouteController struct { diff --git a/api-gateway/routes/prow.routes.go b/persys-gateway/routes/prow.routes.go similarity index 95% rename from api-gateway/routes/prow.routes.go rename to persys-gateway/routes/prow.routes.go index 9e8136f..c902534 100644 --- a/api-gateway/routes/prow.routes.go +++ b/persys-gateway/routes/prow.routes.go @@ -2,7 +2,7 @@ package routes import ( "github.com/gin-gonic/gin" - "github.com/persys-dev/persys-cloud/api-gateway/controllers" + "github.com/persys-dev/persys-cloud/persys-gateway/controllers" ) type ProwRouteController struct { diff --git a/api-gateway/sample.env b/persys-gateway/sample.env similarity index 100% rename from api-gateway/sample.env rename to persys-gateway/sample.env diff --git a/api-gateway/services/auth.impl.service.go b/persys-gateway/services/auth.impl.service.go similarity index 98% rename from api-gateway/services/auth.impl.service.go rename to persys-gateway/services/auth.impl.service.go index 8815b6f..8590b4c 100755 --- a/api-gateway/services/auth.impl.service.go +++ b/persys-gateway/services/auth.impl.service.go @@ -9,7 +9,7 @@ import ( jwtlib "github.com/dgrijalva/jwt-go" "github.com/dgrijalva/jwt-go/request" "github.com/gin-gonic/gin" - "github.com/persys-dev/persys-cloud/api-gateway/models" + "github.com/persys-dev/persys-cloud/persys-gateway/models" //"github.com/wpcodevo/golang-mongodb/utils" "go.mongodb.org/mongo-driver/bson" diff --git a/api-gateway/services/auth.service.go b/persys-gateway/services/auth.service.go similarity index 84% rename from api-gateway/services/auth.service.go rename to persys-gateway/services/auth.service.go index 5099d5c..1d5ad1d 100755 --- a/api-gateway/services/auth.service.go +++ b/persys-gateway/services/auth.service.go @@ -2,7 +2,7 @@ package services import ( "github.com/gin-gonic/gin" - "github.com/persys-dev/persys-cloud/api-gateway/models" + "github.com/persys-dev/persys-cloud/persys-gateway/models" ) type AuthService interface { diff --git a/api-gateway/services/certmanager.go b/persys-gateway/services/certmanager.go similarity index 100% rename from api-gateway/services/certmanager.go rename to persys-gateway/services/certmanager.go diff --git a/api-gateway/services/github.impl.service.go b/persys-gateway/services/github.impl.service.go similarity index 96% rename from api-gateway/services/github.impl.service.go rename to persys-gateway/services/github.impl.service.go index 7e58469..285d25d 100644 --- a/api-gateway/services/github.impl.service.go +++ b/persys-gateway/services/github.impl.service.go @@ -3,7 +3,7 @@ package services import ( "context" "github.com/google/go-github/github" - "github.com/persys-dev/persys-cloud/api-gateway/models" + "github.com/persys-dev/persys-cloud/persys-gateway/models" "go.mongodb.org/mongo-driver/bson" "time" diff --git a/api-gateway/services/github.service.go b/persys-gateway/services/github.service.go similarity index 81% rename from api-gateway/services/github.service.go rename to persys-gateway/services/github.service.go index 48452fe..9db048d 100644 --- a/api-gateway/services/github.service.go +++ b/persys-gateway/services/github.service.go @@ -2,7 +2,7 @@ package services import ( "github.com/google/go-github/github" - "github.com/persys-dev/persys-cloud/api-gateway/models" + "github.com/persys-dev/persys-cloud/persys-gateway/models" "go.mongodb.org/mongo-driver/bson" ) diff --git a/api-gateway/services/prow.service.go b/persys-gateway/services/scheduler.service.go similarity index 99% rename from api-gateway/services/prow.service.go rename to persys-gateway/services/scheduler.service.go index 3fcdd08..39bdf2e 100644 --- a/api-gateway/services/prow.service.go +++ b/persys-gateway/services/scheduler.service.go @@ -13,7 +13,7 @@ import ( "time" "github.com/gin-gonic/gin" - "github.com/persys-dev/persys-cloud/api-gateway/config" + "github.com/persys-dev/persys-cloud/persys-gateway/config" gootelhttp "go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp" "go.opentelemetry.io/otel" ) diff --git a/api-gateway/tests/auth_test.go b/persys-gateway/tests/auth_test.go similarity index 93% rename from api-gateway/tests/auth_test.go rename to persys-gateway/tests/auth_test.go index 2feb31a..867a7dc 100644 --- a/api-gateway/tests/auth_test.go +++ b/persys-gateway/tests/auth_test.go @@ -3,9 +3,9 @@ package tests import ( "context" "github.com/gin-gonic/gin" - "github.com/persys-dev/persys-cloud/api-gateway/controllers" - "github.com/persys-dev/persys-cloud/api-gateway/routes" - "github.com/persys-dev/persys-cloud/api-gateway/services" + "github.com/persys-dev/persys-cloud/persys-gateway/controllers" + "github.com/persys-dev/persys-cloud/persys-gateway/routes" + "github.com/persys-dev/persys-cloud/persys-gateway/services" "github.com/stretchr/testify/assert" "go.mongodb.org/mongo-driver/mongo" "go.mongodb.org/mongo-driver/mongo/options" diff --git a/api-gateway/tests/github_test.go b/persys-gateway/tests/github_test.go similarity index 100% rename from api-gateway/tests/github_test.go rename to persys-gateway/tests/github_test.go diff --git a/api-gateway/utils/audit.go b/persys-gateway/utils/audit.go similarity index 97% rename from api-gateway/utils/audit.go rename to persys-gateway/utils/audit.go index dbd393d..ffbb341 100644 --- a/api-gateway/utils/audit.go +++ b/persys-gateway/utils/audit.go @@ -6,7 +6,7 @@ import ( "os" "time" - "github.com/persys-dev/persys-cloud/api-gateway/config" // Adjust import based on actual config package + "github.com/persys-dev/persys-cloud/persys-gateway/config" // Adjust import based on actual config package "go.opentelemetry.io/otel/trace" "go.uber.org/zap" "go.uber.org/zap/zapcore" diff --git a/api-gateway/utils/token.go b/persys-gateway/utils/token.go similarity index 100% rename from api-gateway/utils/token.go rename to persys-gateway/utils/token.go diff --git a/prow/Dockerfile b/persys-scheduler/Dockerfile similarity index 77% rename from prow/Dockerfile rename to persys-scheduler/Dockerfile index c2bafd2..b50d758 100644 --- a/prow/Dockerfile +++ b/persys-scheduler/Dockerfile @@ -12,7 +12,7 @@ RUN go mod download COPY . . # Build the application -RUN CGO_ENABLED=0 GOOS=linux go build -o prow ./cmd/scheduler +RUN CGO_ENABLED=0 GOOS=linux go build -o persys-scheduler ./cmd/scheduler # Stage 2: Create the final image FROM alpine:latest @@ -21,10 +21,10 @@ FROM alpine:latest WORKDIR /root/ # Copy the binary from the builder stage -COPY --from=builder /app/prow . +COPY --from=builder /app/persys-scheduler . # Expose the port the scheduler listens on EXPOSE 8084 # Command to run the scheduler -CMD ["./prow"] \ No newline at end of file +CMD ["./persys-scheduler"] \ No newline at end of file diff --git a/prow/Makefile b/persys-scheduler/Makefile similarity index 97% rename from prow/Makefile rename to persys-scheduler/Makefile index 16aae07..4e3110d 100644 --- a/prow/Makefile +++ b/persys-scheduler/Makefile @@ -1,10 +1,10 @@ # Makefile for Go project # Variables -BINARY_NAME = prow +BINARY_NAME = persys-scheduler BINARY_DIR = bin BINARY_PATH = $(BINARY_DIR)/$(BINARY_NAME) -IMAGE_NAME=persys-prow +IMAGE_NAME=persys-scheduler IMAGE_TAG=latest GO = go GOFLAGS = -v diff --git a/persys-scheduler/README.md b/persys-scheduler/README.md new file mode 100644 index 0000000..897c7f1 --- /dev/null +++ b/persys-scheduler/README.md @@ -0,0 +1,77 @@ +# Persys Scheduler + +`persys-scheduler` is the scheduler/control-plane service for Persys workloads. + +## Responsibilities + +- Accept node registration + heartbeat over gRPC. +- Persist node/workload state in etcd. +- Select nodes for workloads based on status, labels, capabilities, and available resources. +- Reconcile desired state (`Running`/`Stopped`/`Deleted`) with actual state. +- Track retries, assignments, reconciliation records, and events. + +## API + +Proto: `api/proto/control.proto` +Service: `persys.control.v1.AgentControl` + +Implemented RPCs include: + +- `RegisterNode` +- `Heartbeat` +- `ApplyWorkload` +- `DeleteWorkload` +- `RetryWorkload` +- `ListNodes` +- `GetNode` +- `ListWorkloads` +- `GetWorkload` +- `GetClusterSummary` + +## Default ports + +- Metrics + health HTTP: `:8084` +- gRPC control plane: `:8085` + +Health endpoints: + +- `GET /health` +- `GET /metrics` + +## Local run + +Start etcd first, then run scheduler. + +```bash +./hack/start-etcd-docker.sh +cd persys-scheduler +go run ./cmd/scheduler -insecure +``` + +`-insecure` disables mTLS for local testing. + +## Key environment variables + +See `sample.env`. + +Most used: + +- `ETCD_ENDPOINTS` (default `localhost:2379`) +- `DOMAIN` (default `persys.local`) +- `GRPC_PORT` (default `8085`) +- `METRICS_PORT` (default `8084`) + +For mTLS mode: + +- `CA_FILE` +- `CFSSL_API_URL` +- `CERT_COMMON_NAME` +- `CERT_ORGANIZATION` + +## Build and test + +```bash +cd persys-scheduler +go build ./cmd/scheduler +go test ./... +``` diff --git a/persys-scheduler/api/proto/agent.proto b/persys-scheduler/api/proto/agent.proto new file mode 100644 index 0000000..328e2f0 --- /dev/null +++ b/persys-scheduler/api/proto/agent.proto @@ -0,0 +1,216 @@ +syntax = "proto3"; + +package persys.agent.v1; + +option go_package = "github.com/persys-dev/persys-cloud/persys-scheduler/internal/agentpb;agentpb"; + +// AgentService defines the gRPC interface for workload management +service AgentService { + // ApplyWorkload creates or updates a workload + rpc ApplyWorkload(ApplyWorkloadRequest) returns (ApplyWorkloadResponse); + + // DeleteWorkload removes a workload + rpc DeleteWorkload(DeleteWorkloadRequest) returns (DeleteWorkloadResponse); + + // GetWorkloadStatus retrieves current workload status + rpc GetWorkloadStatus(GetWorkloadStatusRequest) returns (GetWorkloadStatusResponse); + + // ListWorkloads returns all managed workloads + rpc ListWorkloads(ListWorkloadsRequest) returns (ListWorkloadsResponse); + + // HealthCheck returns agent health status + rpc HealthCheck(HealthCheckRequest) returns (HealthCheckResponse); + + // ListActions returns action/task history tracked by the agent since startup + rpc ListActions(ListActionsRequest) returns (ListActionsResponse); +} + +message ApplyWorkloadRequest { + string id = 1; + WorkloadType type = 2; + string revision_id = 3; + DesiredState desired_state = 4; + WorkloadSpec spec = 5; +} + +message ApplyWorkloadResponse { + bool applied = 1; + bool skipped = 2; // true if revision already applied + string message = 3; + WorkloadStatus status = 4; +} + +message DeleteWorkloadRequest { + string id = 1; +} + +message DeleteWorkloadResponse { + bool success = 1; + string message = 2; +} + +message GetWorkloadStatusRequest { + string id = 1; +} + +message GetWorkloadStatusResponse { + WorkloadStatus status = 1; +} + +message ListWorkloadsRequest { + WorkloadType type = 1; // optional filter +} + +message ListWorkloadsResponse { + repeated WorkloadStatus workloads = 1; +} + +message HealthCheckRequest {} + +message HealthCheckResponse { + bool healthy = 1; + string version = 2; + map runtime_status = 3; + double memory_utilization = 4; // Memory utilization percentage (0-100) + double cpu_utilization = 5; // CPU utilization percentage (0-100) + double disk_utilization = 6; // Disk utilization percentage for root mount (0-100) +} + +message ListActionsRequest { + string workload_id = 1; // optional filter + string action_type = 2; // optional filter: apply_workload, delete_workload, ... + string status = 3; // optional filter: pending, running, completed, failed + int32 limit = 4; // optional max number of actions (0 = all) + bool newest_first = 5; // if true sorts by created_at descending +} + +message AgentAction { + string task_id = 1; + string workload_id = 2; + string action_type = 3; + string status = 4; + string error = 5; + int64 created_at = 6; + int64 started_at = 7; + int64 ended_at = 8; +} + +message ListActionsResponse { + repeated AgentAction actions = 1; +} + +enum WorkloadType { + WORKLOAD_TYPE_UNSPECIFIED = 0; + WORKLOAD_TYPE_CONTAINER = 1; + WORKLOAD_TYPE_COMPOSE = 2; + WORKLOAD_TYPE_VM = 3; +} + +enum DesiredState { + DESIRED_STATE_UNSPECIFIED = 0; + DESIRED_STATE_RUNNING = 1; + DESIRED_STATE_STOPPED = 2; +} + +enum ActualState { + ACTUAL_STATE_UNSPECIFIED = 0; + ACTUAL_STATE_PENDING = 1; + ACTUAL_STATE_RUNNING = 2; + ACTUAL_STATE_STOPPED = 3; + ACTUAL_STATE_FAILED = 4; + ACTUAL_STATE_UNKNOWN = 5; +} + +message WorkloadSpec { + oneof spec { + ContainerSpec container = 1; + ComposeSpec compose = 2; + VMSpec vm = 3; + } +} + +message ContainerSpec { + string image = 1; + repeated string command = 2; + repeated string args = 3; + map env = 4; + repeated VolumeMount volumes = 5; + repeated PortMapping ports = 6; + ResourceLimits resources = 7; + RestartPolicy restart_policy = 8; + map labels = 9; +} + +message ComposeSpec { + string project_name = 1; + string compose_yaml = 2; // base64 encoded docker-compose.yml + map env = 3; +} + +message VMSpec { + string name = 1; + int32 vcpus = 2; + int64 memory_mb = 3; + repeated DiskConfig disks = 4; + repeated NetworkConfig networks = 5; + string cloud_init = 6; // optional cloud-init user-data (YAML content) + map metadata = 7; + CloudInitConfig cloud_init_config = 8; // advanced cloud-init settings +} + +message CloudInitConfig { + string user_data = 1; // cloud-init user-data script + string meta_data = 2; // cloud-init meta-data (JSON) + string network_config = 3; // cloud-init network config (YAML) + string vendor_data = 4; // cloud-init vendor-data +} + +message VolumeMount { + string host_path = 1; + string container_path = 2; + bool read_only = 3; +} + +message PortMapping { + int32 host_port = 1; + int32 container_port = 2; + string protocol = 3; // tcp or udp +} + +message ResourceLimits { + int64 cpu_shares = 1; + int64 memory_bytes = 2; + int64 memory_swap_bytes = 3; +} + +message RestartPolicy { + string policy = 1; // no, always, on-failure, unless-stopped + int32 max_retry_count = 2; +} + +message DiskConfig { + string path = 1; // path to disk image or ISO + string device = 2; // vda, vdb, etc. + string format = 3; // qcow2, raw, iso + int64 size_gb = 4; + string type = 5; // disk or cdrom (for ISO) + bool boot = 6; // true if this is the boot disk/ISO +} + +message NetworkConfig { + string network = 1; // network name or bridge + string mac_address = 2; + string ip_address = 3; // optional static IP +} + +message WorkloadStatus { + string id = 1; + WorkloadType type = 2; + string revision_id = 3; + DesiredState desired_state = 4; + ActualState actual_state = 5; + string message = 6; + int64 created_at = 7; + int64 updated_at = 8; + map metadata = 9; +} diff --git a/persys-scheduler/api/proto/control.proto b/persys-scheduler/api/proto/control.proto new file mode 100644 index 0000000..79f022f --- /dev/null +++ b/persys-scheduler/api/proto/control.proto @@ -0,0 +1,298 @@ +syntax = "proto3"; + +package persys.control.v1; + +option go_package = "github.com/persys-dev/persys/api/control/v1;controlv1"; + +import "google/protobuf/timestamp.proto"; + +service AgentControl { + // Registration + rpc RegisterNode(RegisterNodeRequest) returns (RegisterNodeResponse); + + // Heartbeat + rpc Heartbeat(HeartbeatRequest) returns (HeartbeatResponse); + + // Workload lifecycle + rpc ApplyWorkload(ApplyWorkloadRequest) returns (ApplyWorkloadResponse); + rpc DeleteWorkload(DeleteWorkloadRequest) returns (DeleteWorkloadResponse); + + // Retry trigger + rpc RetryWorkload(RetryWorkloadRequest) returns (RetryWorkloadResponse); + + // Cluster and node management visibility + rpc ListNodes(ListNodesRequest) returns (ListNodesResponse); + rpc GetNode(GetNodeRequest) returns (GetNodeResponse); + rpc ListWorkloads(ListWorkloadsRequest) returns (ListWorkloadsResponse); + rpc GetWorkload(GetWorkloadRequest) returns (GetWorkloadResponse); + rpc GetClusterSummary(GetClusterSummaryRequest) returns (GetClusterSummaryResponse); + + // Optional future streaming channel + rpc ControlStream(stream ControlMessage) returns (stream ControlMessage); +} + +message RegisterNodeRequest { + string node_id = 1; + NodeCapabilities capabilities = 2; + map labels = 3; + string agent_version = 4; + string grpc_endpoint = 5; + string cluster_id = 6; + google.protobuf.Timestamp timestamp = 7; +} + +message NodeCapabilities { + int64 cpu_total_millicores = 1; + int64 memory_total_mb = 2; + repeated StoragePool storage_pools = 3; + repeated string supported_workload_types = 4; // container, compose, vm +} + +message StoragePool { + string name = 1; + string type = 2; // local, nfs, iscsi + int64 total_gb = 3; +} + +message RegisterNodeResponse { + bool accepted = 1; + string reason = 2; + int32 heartbeat_interval_seconds = 3; + google.protobuf.Timestamp lease_expires_at = 4; +} + +message HeartbeatRequest { + string node_id = 1; + NodeUsage usage = 2; + repeated WorkloadStatus workload_statuses = 3; + google.protobuf.Timestamp timestamp = 4; +} + +message NodeUsage { + int64 cpu_allocated_millicores = 1; + int64 cpu_used_millicores = 2; + int64 memory_allocated_mb = 3; + int64 memory_used_mb = 4; + int64 disk_allocated_gb = 5; + int64 disk_used_gb = 6; +} + +message HeartbeatResponse { + bool acknowledged = 1; + bool drain_node = 2; + google.protobuf.Timestamp lease_expires_at = 3; +} + +message ApplyWorkloadRequest { + string workload_id = 1; + WorkloadSpec spec = 2; + + // Compatibility fields aligned with existing agent apply semantics. + string revision_id = 10; + string desired_state = 11; // Running | Stopped +} + +message ApplyWorkloadResponse { + bool success = 1; + FailureReason failure_reason = 2; + string error_message = 3; +} + +message DeleteWorkloadRequest { + string workload_id = 1; +} + +message DeleteWorkloadResponse { + bool success = 1; + string error_message = 2; +} + +message WorkloadSpec { + string type = 1; // container, compose, vm + ResourceRequirements resources = 2; + + oneof workload { + ContainerSpec container = 10; + ComposeSpec compose = 11; + VMSpec vm = 12; + } + + map metadata = 20; +} + +message ResourceRequirements { + int64 cpu_millicores = 1; + int64 memory_mb = 2; + int64 disk_gb = 3; +} + +message ContainerSpec { + string image = 1; + repeated string command = 2; + map env = 3; + repeated VolumeMount volumes = 4; + repeated Port ports = 5; + string restart_policy = 6; + bool privileged = 7; +} + +message VolumeMount { + string host_path = 1; + string container_path = 2; + bool read_only = 3; +} + +message Port { + int32 host_port = 1; + int32 container_port = 2; + string protocol = 3; // tcp or udp +} + +message ComposeSpec { + string source_type = 1; // git or inline + string git_repo = 2; + string git_ref = 3; + string inline_yaml = 4; + map env = 5; +} + +message VMSpec { + int32 vcpus = 1; + int64 memory_mb = 2; + repeated DiskConfig disks = 3; + repeated NetworkConfig networks = 4; + CloudInitConfig cloud_init = 5; + string os_image = 6; +} + +message DiskConfig { + string pool_name = 1; + int64 size_gb = 2; + string mount_point = 3; +} + +message NetworkConfig { + string bridge = 1; + bool dhcp = 2; + string static_ip = 3; +} + +message CloudInitConfig { + string user_data = 1; + string meta_data = 2; + string network_config = 3; +} + +message WorkloadStatus { + string workload_id = 1; + string state = 2; // Running, Stopped, Failed + FailureReason failure_reason = 3; + string message = 4; + google.protobuf.Timestamp last_transition = 5; +} + +enum FailureReason { + FAILURE_REASON_UNSPECIFIED = 0; + IMAGE_PULL_FAILED = 1; + IMAGE_NOT_FOUND = 2; + INSUFFICIENT_RESOURCES = 3; + INVALID_SPEC = 4; + RUNTIME_ERROR = 5; + NETWORK_ERROR = 6; + STORAGE_ERROR = 7; + VM_BOOT_FAILED = 8; +} + +message RetryWorkloadRequest { + string workload_id = 1; +} + +message RetryWorkloadResponse { + bool accepted = 1; +} + +message ListNodesRequest { + string status = 1; // optional filter: Ready | NotReady | Draining +} + +message GetNodeRequest { + string node_id = 1; +} + +message ListNodesResponse { + repeated NodeView nodes = 1; +} + +message GetNodeResponse { + NodeView node = 1; +} + +message NodeView { + string node_id = 1; + string status = 2; + string status_reason = 3; + string status_updated_by = 4; + google.protobuf.Timestamp status_updated_at = 5; + google.protobuf.Timestamp last_heartbeat = 6; + string grpc_endpoint = 7; + double total_cpu_cores = 8; + double available_cpu_cores = 9; + int64 total_memory_mb = 10; + int64 available_memory_mb = 11; + repeated string supported_workload_types = 12; + map labels = 13; +} + +message ListWorkloadsRequest { + string node_id = 1; // optional filter + string status = 2; // optional filter +} + +message GetWorkloadRequest { + string workload_id = 1; +} + +message ListWorkloadsResponse { + repeated WorkloadView workloads = 1; +} + +message GetWorkloadResponse { + WorkloadView workload = 1; +} + +message WorkloadView { + string workload_id = 1; + string type = 2; + string desired_state = 3; + string status = 4; + string assigned_node_id = 5; + string revision_id = 6; + int32 retry_attempts = 7; + int32 retry_max_attempts = 8; + google.protobuf.Timestamp retry_next_at = 9; + string failure_reason = 10; + google.protobuf.Timestamp last_updated = 11; +} + +message GetClusterSummaryRequest {} + +message GetClusterSummaryResponse { + int32 total_nodes = 1; + int32 ready_nodes = 2; + int32 not_ready_nodes = 3; + int32 total_workloads = 4; + int32 running_workloads = 5; + int32 pending_workloads = 6; + int32 failed_workloads = 7; + int32 deleted_workloads = 8; + google.protobuf.Timestamp generated_at = 9; +} + +message ControlMessage { + oneof message { + RegisterNodeRequest register = 1; + HeartbeatRequest heartbeat = 2; + ApplyWorkloadRequest apply = 3; + DeleteWorkloadRequest delete = 4; + } +} diff --git a/persys-scheduler/cmd/scheduler/main.go b/persys-scheduler/cmd/scheduler/main.go new file mode 100644 index 0000000..f2ab934 --- /dev/null +++ b/persys-scheduler/cmd/scheduler/main.go @@ -0,0 +1,190 @@ +package main + +import ( + "context" + "crypto/tls" + "crypto/x509" + "flag" + "fmt" + "net" + "net/http" + "os" + "os/signal" + "syscall" + "time" + + "github.com/persys-dev/persys-cloud/persys-scheduler/internal/auth" + controlv1 "github.com/persys-dev/persys-cloud/persys-scheduler/internal/controlv1" + "github.com/persys-dev/persys-cloud/persys-scheduler/internal/grpcapi" + "github.com/persys-dev/persys-cloud/persys-scheduler/internal/logging" + metricspkg "github.com/persys-dev/persys-cloud/persys-scheduler/internal/metrics" + "github.com/persys-dev/persys-cloud/persys-scheduler/internal/scheduler" + "github.com/persys-dev/persys-cloud/persys-scheduler/internal/telemetry" + "github.com/prometheus/client_golang/prometheus/promhttp" + "go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc" + gootelhttp "go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp" + "google.golang.org/grpc" + "google.golang.org/grpc/credentials" + "google.golang.org/grpc/reflection" +) + +func main() { + logger := logging.C("cmd.scheduler") + insecure := flag.Bool("insecure", false, "run scheduler gRPC without mTLS (testing only)") + flag.Parse() + + metricspkg.Register() + + otelShutdown, err := telemetry.SetupOpenTelemetry(context.Background(), "persys-scheduler") + if err != nil { + logger.WithError(err).Fatal("failed to initialize OpenTelemetry") + } + + caFile := os.Getenv("CA_FILE") + cfsslURL := os.Getenv("CFSSL_API_URL") + commonName := os.Getenv("CERT_COMMON_NAME") + organization := os.Getenv("CERT_ORGANIZATION") + + if caFile == "" { + caFile = "/etc/prow/certs/ca.pem" + } + if cfsslURL == "" { + cfsslURL = "https://persys-cfssl:8888" + } + if commonName == "" { + commonName = "persys-scheduler" + } + if organization == "" { + organization = "persys" + } + + var tlsConfig *tls.Config + if !*insecure { + certManager := auth.NewCertificateManager(caFile, cfsslURL, commonName, organization) + if err := certManager.EnsureCertificate(); err != nil { + logger.WithError(err).Fatal("failed to ensure certificate") + } + cert, err := tls.LoadX509KeyPair(certManager.CertPath, certManager.KeyPath) + if err != nil { + logger.WithError(err).Fatal("failed to load certificate pair") + } + caCert, err := os.ReadFile(caFile) + if err != nil { + logger.WithError(err).WithField("ca_file", caFile).Fatal("failed to read CA cert") + } + caCertPool := x509.NewCertPool() + if !caCertPool.AppendCertsFromPEM(caCert) { + logger.Fatal("failed to append CA cert to pool") + } + tlsConfig = &tls.Config{ + Certificates: []tls.Certificate{cert}, + ClientCAs: caCertPool, + ClientAuth: tls.RequireAndVerifyClientCert, + MinVersion: tls.VersionTLS12, + } + } else { + logger.Warn("starting scheduler in insecure mode: mTLS disabled") + } + + sched, err := scheduler.NewScheduler() + if err != nil { + logger.WithError(err).Fatal("failed to initialize scheduler") + } + defer sched.Close() + if err := sched.RefreshStateMetrics(); err != nil { + logger.WithError(err).Warn("failed to initialize scheduler state metrics") + } + + ctx, cancel := context.WithCancel(context.Background()) + defer cancel() + + sched.StartMonitoring(ctx) + sched.StartReconciliation(ctx) + + grpcPort := os.Getenv("GRPC_PORT") + if grpcPort == "" { + grpcPort = "8085" + } + lis, err := net.Listen("tcp", ":"+grpcPort) + if err != nil { + logger.WithError(err).WithField("port", grpcPort).Fatal("failed to listen on gRPC port") + } + + grpcOpts := []grpc.ServerOption{ + grpc.StatsHandler(otelgrpc.NewServerHandler()), + grpc.UnaryInterceptor(metricspkg.GRPCUnaryServerInterceptor()), + grpc.StreamInterceptor(metricspkg.GRPCStreamServerInterceptor()), + } + var grpcServer *grpc.Server + if *insecure { + grpcServer = grpc.NewServer(grpcOpts...) + } else { + grpcServer = grpc.NewServer(append(grpcOpts, grpc.Creds(credentials.NewTLS(tlsConfig)))...) + } + controlv1.RegisterAgentControlServer(grpcServer, grpcapi.NewService(sched)) + reflection.Register(grpcServer) + + metricsPort := os.Getenv("METRICS_PORT") + if metricsPort == "" { + metricsPort = "8084" + } + metricsMux := http.NewServeMux() + metricsMux.Handle("/metrics", gootelhttp.NewHandler(promhttp.Handler(), "scheduler.metrics")) + metricsMux.Handle("/health", gootelhttp.NewHandler(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + w.WriteHeader(http.StatusOK) + _, _ = w.Write([]byte("ok")) + }), "scheduler.health")) + metricsServer := &http.Server{Addr: ":" + metricsPort, Handler: metricsMux} + + serverErrCh := make(chan error, 2) + + go func() { + logger.WithField("port", grpcPort).Info("starting scheduler gRPC server") + if err := grpcServer.Serve(lis); err != nil { + serverErrCh <- fmt.Errorf("gRPC server failed: %w", err) + } + }() + go func() { + logger.WithField("port", metricsPort).Info("starting scheduler metrics server") + if err := metricsServer.ListenAndServe(); err != nil && err != http.ErrServerClosed { + serverErrCh <- fmt.Errorf("metrics server failed: %w", err) + } + }() + + sigCtx, stopSignals := signal.NotifyContext(context.Background(), syscall.SIGINT, syscall.SIGTERM) + defer stopSignals() + + select { + case <-sigCtx.Done(): + logger.Info("shutdown signal received") + case err := <-serverErrCh: + logger.WithError(err).Error("server exited unexpectedly") + } + + logger.Info("shutting down scheduler servers") + cancel() + + shutdownCtx, shutdownCancel := context.WithTimeout(context.Background(), 10*time.Second) + defer shutdownCancel() + grpcStopped := make(chan struct{}) + go func() { + grpcServer.GracefulStop() + close(grpcStopped) + }() + select { + case <-grpcStopped: + case <-shutdownCtx.Done(): + logger.Warn("gRPC graceful stop timed out; forcing stop") + grpcServer.Stop() + } + if err := metricsServer.Shutdown(shutdownCtx); err != nil { + logger.WithError(err).Error("metrics server shutdown error") + } + if err := otelShutdown(shutdownCtx); err != nil { + logger.WithError(err).Warn("OpenTelemetry shutdown error") + } + + if ok := sched.WaitForBackground(5 * time.Second); !ok { + logger.Warn("timed out waiting for scheduler background workers to stop") + } +} diff --git a/persys-scheduler/cmd/smoke-client/main.go b/persys-scheduler/cmd/smoke-client/main.go new file mode 100644 index 0000000..892633b --- /dev/null +++ b/persys-scheduler/cmd/smoke-client/main.go @@ -0,0 +1,249 @@ +package main + +import ( + "context" + "encoding/json" + "flag" + "fmt" + "log" + "time" + + controlv1 "github.com/persys-dev/persys-cloud/persys-scheduler/internal/controlv1" + "google.golang.org/grpc" + "google.golang.org/grpc/credentials/insecure" + "google.golang.org/protobuf/types/known/timestamppb" +) + +func main() { + op := flag.String("op", "", "operation: register-node | heartbeat | apply-container | apply-vm | delete-workload | retry-workload | list-nodes | get-node | list-workloads | get-workload | cluster-summary") + schedulerAddr := flag.String("scheduler", "127.0.0.1:8085", "scheduler gRPC address") + timeout := flag.Duration("timeout", 20*time.Second, "rpc timeout") + + nodeID := flag.String("node-id", fmt.Sprintf("node-%d", time.Now().Unix()), "node id") + nodeEndpoint := flag.String("node-endpoint", "127.0.0.1:50051", "agent gRPC endpoint for register-node") + clusterID := flag.String("cluster-id", "default", "cluster id for register-node") + agentVersion := flag.String("agent-version", "dev", "agent version for register-node") + cpuTotal := flag.Int64("cpu-total", 4000, "node total millicores") + memTotal := flag.Int64("mem-total", 8192, "node total memory MB") + supportedTypes := flag.String("supported-types", "container,compose", "supported workload types CSV for register-node (e.g. container,compose,vm)") + + cpuAllocated := flag.Int64("cpu-allocated", 1000, "heartbeat allocated millicores") + cpuUsed := flag.Int64("cpu-used", 400, "heartbeat used millicores") + memAllocated := flag.Int64("mem-allocated", 1024, "heartbeat allocated memory MB") + memUsed := flag.Int64("mem-used", 512, "heartbeat used memory MB") + diskAllocated := flag.Int64("disk-allocated", 10, "heartbeat allocated disk GB") + diskUsed := flag.Int64("disk-used", 3, "heartbeat used disk GB") + + workloadID := flag.String("workload-id", fmt.Sprintf("wl-%d", time.Now().Unix()), "workload id") + revisionID := flag.String("revision-id", "r1", "workload revision id") + desiredState := flag.String("desired-state", "Running", "workload desired state") + wCPU := flag.Int64("w-cpu", 250, "workload requested millicores") + wMem := flag.Int64("w-mem", 256, "workload requested memory MB") + wDisk := flag.Int64("w-disk", 2, "workload requested disk GB") + + containerImage := flag.String("container-image", "alpine:latest", "container image") + containerCmd := flag.String("container-cmd", "sleep,60", "container command CSV") + containerRestart := flag.String("container-restart", "unless-stopped", "container restart policy") + + vmVCPUs := flag.Int("vm-vcpus", 2, "vm vcpus") + vmMemory := flag.Int64("vm-memory", 2048, "vm memory MB") + vmImage := flag.String("vm-image", "ubuntu-22.04", "vm os image") + vmDiskPool := flag.String("vm-disk-pool", "local", "vm disk pool") + vmDiskSize := flag.Int64("vm-disk-size", 20, "vm primary disk size GB") + vmBridge := flag.String("vm-bridge", "br0", "vm network bridge") + vmDHCP := flag.Bool("vm-dhcp", true, "vm network dhcp") + filterStatus := flag.String("status", "", "optional status filter for list-nodes/list-workloads") + filterNodeID := flag.String("filter-node-id", "", "optional node id filter for list-workloads") + + flag.Parse() + if *op == "" { + log.Fatalf("missing -op") + } + + ctx, cancel := context.WithTimeout(context.Background(), *timeout) + defer cancel() + + conn, err := grpc.DialContext(ctx, *schedulerAddr, + grpc.WithTransportCredentials(insecure.NewCredentials()), + grpc.WithBlock(), + ) + if err != nil { + log.Fatalf("failed to connect scheduler %s: %v", *schedulerAddr, err) + } + defer conn.Close() + + client := controlv1.NewAgentControlClient(conn) + + switch *op { + case "register-node": + resp, err := client.RegisterNode(ctx, &controlv1.RegisterNodeRequest{ + NodeId: *nodeID, + Capabilities: &controlv1.NodeCapabilities{ + CpuTotalMillicores: *cpuTotal, + MemoryTotalMb: *memTotal, + SupportedWorkloadTypes: splitCSV(*supportedTypes), + }, + AgentVersion: *agentVersion, + GrpcEndpoint: *nodeEndpoint, + ClusterId: *clusterID, + Timestamp: timestamppb.Now(), + }) + if err != nil { + log.Fatalf("register-node failed: %v", err) + } + log.Printf("register-node accepted=%v reason=%q heartbeat_interval=%d", resp.GetAccepted(), resp.GetReason(), resp.GetHeartbeatIntervalSeconds()) + case "heartbeat": + resp, err := client.Heartbeat(ctx, &controlv1.HeartbeatRequest{ + NodeId: *nodeID, + Usage: &controlv1.NodeUsage{ + CpuAllocatedMillicores: *cpuAllocated, + CpuUsedMillicores: *cpuUsed, + MemoryAllocatedMb: *memAllocated, + MemoryUsedMb: *memUsed, + DiskAllocatedGb: *diskAllocated, + DiskUsedGb: *diskUsed, + }, + Timestamp: timestamppb.Now(), + }) + if err != nil { + log.Fatalf("heartbeat failed: %v", err) + } + log.Printf("heartbeat acknowledged=%v drain_node=%v", resp.GetAcknowledged(), resp.GetDrainNode()) + case "apply-container": + resp, err := client.ApplyWorkload(ctx, &controlv1.ApplyWorkloadRequest{ + WorkloadId: *workloadID, + RevisionId: *revisionID, + DesiredState: *desiredState, + Spec: &controlv1.WorkloadSpec{ + Type: "container", + Resources: &controlv1.ResourceRequirements{ + CpuMillicores: *wCPU, + MemoryMb: *wMem, + DiskGb: *wDisk, + }, + Workload: &controlv1.WorkloadSpec_Container{ + Container: &controlv1.ContainerSpec{ + Image: *containerImage, + Command: splitCSV(*containerCmd), + RestartPolicy: *containerRestart, + }, + }, + }, + }) + if err != nil { + log.Fatalf("apply-container failed: %v", err) + } + log.Printf("apply-container success=%v failure_reason=%s error=%q", resp.GetSuccess(), resp.GetFailureReason().String(), resp.GetErrorMessage()) + case "apply-vm": + resp, err := client.ApplyWorkload(ctx, &controlv1.ApplyWorkloadRequest{ + WorkloadId: *workloadID, + RevisionId: *revisionID, + DesiredState: *desiredState, + Spec: &controlv1.WorkloadSpec{ + Type: "vm", + Resources: &controlv1.ResourceRequirements{ + CpuMillicores: *wCPU, + MemoryMb: *wMem, + DiskGb: *wDisk, + }, + Workload: &controlv1.WorkloadSpec_Vm{ + Vm: &controlv1.VMSpec{ + Vcpus: int32(*vmVCPUs), + MemoryMb: *vmMemory, + Disks: []*controlv1.DiskConfig{ + { + PoolName: *vmDiskPool, + SizeGb: *vmDiskSize, + }, + }, + Networks: []*controlv1.NetworkConfig{ + { + Bridge: *vmBridge, + Dhcp: *vmDHCP, + }, + }, + OsImage: *vmImage, + }, + }, + }, + }) + if err != nil { + log.Fatalf("apply-vm failed: %v", err) + } + log.Printf("apply-vm success=%v failure_reason=%s error=%q", resp.GetSuccess(), resp.GetFailureReason().String(), resp.GetErrorMessage()) + case "delete-workload": + resp, err := client.DeleteWorkload(ctx, &controlv1.DeleteWorkloadRequest{ + WorkloadId: *workloadID, + }) + if err != nil { + log.Fatalf("delete-workload failed: %v", err) + } + log.Printf("delete-workload success=%v error=%q", resp.GetSuccess(), resp.GetErrorMessage()) + case "retry-workload": + resp, err := client.RetryWorkload(ctx, &controlv1.RetryWorkloadRequest{ + WorkloadId: *workloadID, + }) + if err != nil { + log.Fatalf("retry-workload failed: %v", err) + } + log.Printf("retry-workload accepted=%v", resp.GetAccepted()) + case "list-nodes": + resp, err := client.ListNodes(ctx, &controlv1.ListNodesRequest{Status: *filterStatus}) + if err != nil { + log.Fatalf("list-nodes failed: %v", err) + } + printJSON(resp) + case "get-node": + resp, err := client.GetNode(ctx, &controlv1.GetNodeRequest{NodeId: *nodeID}) + if err != nil { + log.Fatalf("get-node failed: %v", err) + } + printJSON(resp) + case "list-workloads": + resp, err := client.ListWorkloads(ctx, &controlv1.ListWorkloadsRequest{ + NodeId: *filterNodeID, + Status: *filterStatus, + }) + if err != nil { + log.Fatalf("list-workloads failed: %v", err) + } + printJSON(resp) + case "get-workload": + resp, err := client.GetWorkload(ctx, &controlv1.GetWorkloadRequest{WorkloadId: *workloadID}) + if err != nil { + log.Fatalf("get-workload failed: %v", err) + } + printJSON(resp) + case "cluster-summary": + resp, err := client.GetClusterSummary(ctx, &controlv1.GetClusterSummaryRequest{}) + if err != nil { + log.Fatalf("cluster-summary failed: %v", err) + } + printJSON(resp) + default: + log.Fatalf("unsupported -op %q", *op) + } + fmt.Println("OK") +} + +func splitCSV(s string) []string { + var parts []string + start := 0 + for i := 0; i <= len(s); i++ { + if i == len(s) || s[i] == ',' { + if start < i { + parts = append(parts, s[start:i]) + } + start = i + 1 + } + } + return parts +} + +func printJSON(v interface{}) { + b, err := json.MarshalIndent(v, "", " ") + if err != nil { + log.Fatalf("json marshal failed: %v", err) + } + fmt.Println(string(b)) +} diff --git a/prow/docker-compose.yml b/persys-scheduler/docker-compose.yml similarity index 93% rename from prow/docker-compose.yml rename to persys-scheduler/docker-compose.yml index d4df8ae..4301450 100644 --- a/prow/docker-compose.yml +++ b/persys-scheduler/docker-compose.yml @@ -1,11 +1,12 @@ services: + etcd: image: quay.io/coreos/etcd:v3.5.0 command: etcd --advertise-client-urls http://etcd:2379 --listen-client-urls http://0.0.0.0:2379 ports: - "2379:2379" - persys-prow: + persys-scheduler: build: context: . dockerfile: Dockerfile diff --git a/persys-scheduler/docs/AGENT_CONTROL_INTEGRATION_GUIDE.md b/persys-scheduler/docs/AGENT_CONTROL_INTEGRATION_GUIDE.md new file mode 100644 index 0000000..ab4acfa --- /dev/null +++ b/persys-scheduler/docs/AGENT_CONTROL_INTEGRATION_GUIDE.md @@ -0,0 +1,193 @@ +# Persys Agent Integration Guide + +## Scope + +This document defines how an agent integrates with the scheduler control plane using: + +- `api/proto/control.proto` (`persys.control.v1.AgentControl`) +- `api/proto/agent.proto` (existing workload/runtime model used by scheduler internals) + +The goal is to enable agent-side implementation with deterministic behavior aligned to scheduler design. + +## Endpoints and Transport + +Scheduler serves: + +- gRPC control API on `GRPC_PORT` (default `8085`) +- HTTP metrics/health only on `METRICS_PORT` (default `8084`) + +Security modes: + +- Production: mTLS enabled by default +- Testing: start scheduler with `-insecure` to disable mTLS + +Example test start: + +```bash +go run ./cmd/scheduler -insecure +``` + +## Control Service + +Service: `persys.control.v1.AgentControl` + +RPCs the agent must implement against scheduler: + +1. `RegisterNode` +2. `Heartbeat` +3. `ApplyWorkload` +4. `DeleteWorkload` +5. `RetryWorkload` +6. `ControlStream` (optional/future; currently unimplemented on scheduler) + +## Required Agent Behavior + +### 1. Node registration + +Call `RegisterNode` once on agent startup and after reconnect if lease expires. + +Required fields: + +- `node_id` +- `capabilities.cpu_total_millicores` +- `capabilities.memory_total_mb` +- `supported_workload_types` (`container`, `compose`, `vm` as supported) +- `grpc_endpoint` (host:port reachable by scheduler for scheduler->agent workload RPCs) +- `timestamp` + +Scheduler response: + +- `accepted=true/false` +- `heartbeat_interval_seconds` +- `lease_expires_at` + +Agent action: + +- If not accepted: log and backoff retry registration +- If accepted: schedule heartbeat loop using returned interval + +### 2. Heartbeats + +Send `Heartbeat` periodically. + +Payload: + +- `node_id` +- `usage` (allocated + used cpu/memory/disk) +- `workload_statuses[]` +- `timestamp` + +Scheduler response: + +- `acknowledged` +- `drain_node` +- `lease_expires_at` + +Agent action: + +- If `drain_node=true`, stop accepting new workloads and prepare shutdown/migration mode +- Keep sending heartbeats while connected + +### 3. Workload apply/delete/retry + +Agent calls scheduler control API when driven by external control component using: + +- `ApplyWorkload` +- `DeleteWorkload` +- `RetryWorkload` + +Note: scheduler persists desired state first, then reconciliation executes scheduler->agent runtime actions. + +## Workload Contract Notes + +`ApplyWorkloadRequest` in `control.proto` contains: + +- `workload_id` +- `spec` (`container|compose|vm` oneof) +- `revision_id` (compatibility/idempotency) +- `desired_state` (`Running|Stopped` string) + +Agent-side guidance: + +- Keep `workload_id` stable across retries +- Change `revision_id` only when spec/intent changes +- Preserve exact spec payload for deterministic replay + +## Failure Semantics + +Use `FailureReason` enum when returning apply failures: + +- `IMAGE_PULL_FAILED` +- `IMAGE_NOT_FOUND` +- `INSUFFICIENT_RESOURCES` +- `INVALID_SPEC` +- `RUNTIME_ERROR` +- `NETWORK_ERROR` +- `STORAGE_ERROR` +- `VM_BOOT_FAILED` + +Return `success=false` with `error_message` when failure occurs. + +## Scheduler State Expectations + +Scheduler treats itself as source of truth and expects: + +- registration + heartbeat updates for node health +- workload transitions reported in heartbeat `workload_statuses` +- deterministic state names (`Running`, `Stopped`, `Failed`) + +If heartbeat stops beyond lease window, scheduler marks node not ready and starts failover logic. + +## Recommended Agent Loop + +1. Start agent runtime services. +2. Register with scheduler. +3. Start heartbeat ticker with server-provided interval. +4. Report node usage + workload statuses each tick. +5. Handle reconnect/re-register on transport failure. +6. Continue serving scheduler->agent workload RPCs (`agent.proto` service) in parallel. + +## Relationship to `agent.proto` + +The scheduler currently uses `agent.proto` (`persys.agent.v1.AgentService`) to drive runtime convergence on the node: + +- `ApplyWorkload` +- `DeleteWorkload` +- `GetWorkloadStatus` +- `ListWorkloads` +- `HealthCheck` +- `ListActions` + +Therefore, the agent must expose both: + +1. Client behavior to call scheduler control service (`control.proto`) +2. Server behavior to accept scheduler runtime calls (`agent.proto`) + +## Minimal Implementation Checklist (Agent) + +- [ ] Generate stubs for `control.proto` +- [ ] Implement scheduler client with mTLS + insecure test mode +- [ ] Implement startup `RegisterNode` +- [ ] Implement periodic `Heartbeat` +- [ ] Populate `NodeUsage` from host metrics +- [ ] Populate `WorkloadStatus` from runtime state +- [ ] Map runtime errors to `FailureReason` +- [ ] Keep stable `workload_id` and proper `revision_id` +- [ ] Expose `agent.proto` gRPC server for scheduler runtime operations +- [ ] Add integration tests for reconnect, lease expiry, and heartbeat loss + +## Local Testing Tips + +With scheduler running insecure: + +```bash +go run ./cmd/scheduler -insecure +``` + +Agent can connect without TLS during bring-up, then switch to mTLS once cert flow is ready. + +## Current Limitations + +- `ControlStream` is defined but not implemented by scheduler yet. +- Heartbeat-driven rescheduling policy is scheduler-side and may evolve. +- Multi-cluster federation fields (`cluster_id`) are accepted but not fully enforced yet. diff --git a/persys-scheduler/docs/SCHEDULER_DESIGN_V1.md b/persys-scheduler/docs/SCHEDULER_DESIGN_V1.md new file mode 100644 index 0000000..97beeeb --- /dev/null +++ b/persys-scheduler/docs/SCHEDULER_DESIGN_V1.md @@ -0,0 +1,391 @@ +# Persys Scheduler + +## Detailed Engineering Design Specification (V1) + +## 1. Purpose + +The Persys Scheduler is the control plane for Persys Compute. + +Responsibilities: + +- Own cluster desired state +- Make placement decisions +- Reconcile desired vs actual state +- Manage retries and backoff +- Manage node lifecycle +- Execute deterministic automation hooks +- Serve as federation boundary (future) + +Non-responsibilities: + +- Runtime execution (containers/VMs) +- Image building +- Log storage +- Metrics backend +- Agent-side reconciliation + +## 2. Design Principles + +### 2.1 Single Source of Truth + +- Desired state is stored in etcd. +- In-memory state is cache only. +- Recovery from process restart is etcd-driven. + +### 2.2 Declarative Control + +- User/API declares desired state (`Running`, `Stopped`, `Deleted`). +- Scheduler converges actual state to desired state. + +### 2.3 Idempotent Operations + +- Scheduler to agent operations are idempotent using `(workload_id, revision_id)`. +- Scheduler can retry safely without duplicating runtime artifacts. + +### 2.4 Reconciliation Ownership + +- Scheduler owns convergence logic. +- Agents execute RPCs and report local actual state. + +### 2.5 Deterministic Behavior + +- Placement decision records reason. +- Reconciliation action records reason. +- Retry state is persisted. + +## 3. System Architecture + +```text +API Gateway + -> Persys Scheduler + - API Server + - Placement Engine + - Reconciler + - Retry Engine + - Node Manager + - Automation Rules + -> Persys Agents (gRPC mTLS) +``` + +## 4. Component Design + +### 4.1 API Server + +API server responsibilities: + +- Validate request payloads +- Persist desired state into etcd +- Trigger scheduling/reconciliation workflows +- Expose read APIs for status and inventory + +API server must not issue runtime execution directly. Runtime actions are emitted via scheduler workers. + +Minimum API surface: + +- `POST /workloads` +- `PUT /workloads/{id}` +- `DELETE /workloads/{id}` +- `GET /workloads/{id}` +- `GET /workloads` +- `GET /nodes` +- `POST /workloads/{id}/retry` + +### 4.2 State Store (etcd) + +Key layout: + +```text +/nodes/ +/workloads/ +/assignments/ +/reconciliation/ +/events/ +/retries/ +``` + +Workload record (canonical shape): + +```json +{ + "id": "workload-123", + "type": "container | compose | vm", + "revision_id": "rev-abc", + "desired_state": "Running | Stopped | Deleted", + "assigned_node": "node-1", + "spec": {}, + "resources": { + "cpu": 2, + "memory_mb": 2048, + "disk_gb": 20 + }, + "retry": { + "attempts": 1, + "max_attempts": 5, + "next_retry_at": "2026-02-17T00:00:00Z" + }, + "status": { + "actual_state": "Pending | Running | Stopped | Failed | Unknown", + "last_updated": "2026-02-17T00:00:00Z", + "failure_reason": "" + }, + "metadata": { + "created_at": "2026-02-17T00:00:00Z", + "last_action": "Apply", + "last_error": "" + } +} +``` + +### 4.3 Node Manager + +Responsibilities: + +- Node registration +- Heartbeat processing +- Capacity accounting +- Node readiness transitions +- Eviction trigger when node is unhealthy + +Node record (canonical shape): + +```json +{ + "id": "node-abc", + "status": "Ready | NotReady | Draining", + "resources": { + "cpu_total": 16, + "cpu_allocated": 8, + "memory_total": 32768, + "memory_allocated": 16000, + "disk_total": 500, + "disk_allocated": 200 + }, + "last_heartbeat": "2026-02-17T00:00:00Z", + "labels": { + "rack": "rack-1", + "zone": "zone-a" + } +} +``` + +Health policy: + +- Missing heartbeat for 3 intervals -> `NotReady` +- `NotReady` beyond grace -> workload eviction/reschedule + +### 4.4 Placement Engine + +Inputs: + +- Workload resource requests +- Node available capacity +- Node labels and readiness + +Algorithm (V1): + +1. Filter nodes by: + - `Ready` + - CPU capacity + - Memory capacity + - Disk capacity + - Label constraints +2. Sort by ascending utilization +3. Choose first node +4. Persist assignment and decision metadata + +### 4.5 Reconciliation Engine + +Loop interval: default 5 seconds (configurable). + +For each workload: + +1. Load desired state from etcd +2. Query agent actual state (`GetWorkloadStatus`) +3. Compare desired vs actual +4. Choose action +5. Execute action (`ApplyWorkload`/`DeleteWorkload`) +6. Persist result and timestamps + +Decision matrix: + +| Desired | Actual | Action | +|---------|---------|----------| +| Running | Missing | Apply | +| Running | Stopped | Apply | +| Running | Failed | Recreate | +| Stopped | Running | ApplyStopped | +| Deleted | Exists | Delete | +| Running | Running | NoAction | + +Rules: + +- Do not execute before grace period expires for transitional states. +- Persist reconciliation metadata for every action. +- No tight retry loops inside one cycle. + +### 4.6 Retry Engine + +Retry state is persisted under workload/retry record. + +Backoff schedule: + +- Base: 5s +- Sequence: 5s, 10s, 20s, 40s +- Cap: 120s + +On failure: + +- `attempts += 1` +- `next_retry_at = now + backoff` +- stop when `attempts >= max_attempts` +- mark workload `Failed` +- emit failure event + +### 4.7 Event System + +Event types: + +- `WorkloadScheduled` +- `WorkloadFailed` +- `NodeLost` +- `RetryTriggered` +- `Rescheduled` + +Rules: + +- Events are immutable +- Events are append-only +- Include workload id, node id, reason, timestamp + +### 4.8 Automation Hooks + +Deterministic rule engine only. + +Examples: + +- If node CPU > 85% for 5 minutes -> emit scale-out webhook +- If workload fails 3 times -> emit alert event + +No LLM inference or probabilistic actions. + +## 5. Workload Lifecycle + +### 5.1 Create + +1. API request accepted +2. Validate spec +3. Persist workload (`desired_state=Running`) +4. Placement assigns node +5. Reconciler executes `ApplyWorkload` +6. Persist status transition + +### 5.2 Update + +1. Persist new `spec` and `revision_id` +2. Mark status `Updating` +3. Reconciler applies update +4. Persist terminal state + +### 5.3 Delete + +1. Set `desired_state=Deleted` +2. Reconciler sends `DeleteWorkload` +3. Wait until `GetWorkloadStatus` returns not found +4. Remove workload record + +## 6. Failure Handling + +Agent unreachable: + +- Mark node `NotReady` +- Start grace timer +- Evict/reschedule after grace window + +Workload failure: + +- Persist exact failure reason +- Trigger retry policy +- Mark terminal `Failed` after max retries + +Node crash: + +- Detect via heartbeat timeout +- Mark workloads for rescheduling +- Re-run placement + +## 7. Concurrency Model + +- Worker pool for reconciliation actions +- Context cancellation for all loops and RPCs +- Optimistic etcd writes where feasible +- Avoid global locks across reconciliation workers + +## 8. High Availability (Future) + +- Multiple stateless scheduler replicas +- Shared etcd state +- Leader election via etcd lease +- Only leader executes reconciliation and retries + +## 9. Security + +- Agent communication via mTLS +- Certificates issued by CFSSL +- Certificate rotation supported +- RBAC enforced at API gateway + +## 10. Observability + +Endpoints: + +- `/metrics` +- `/health` + +Core metrics: + +- `scheduling_attempts_total` +- `scheduling_failures_total` +- `reconciliation_actions_total` +- `retry_total` +- `node_unhealthy_total` + +## 11. Scale Targets + +V1 target envelope: + +- hundreds of nodes +- thousands of workloads + +Scale methods: + +- scheduler HA replicas +- etcd clustering +- future sharding/federation + +## 12. Out of Scope + +Scheduler does not: + +- build images +- execute workloads directly +- persist full logs +- reconcile from agent side + +## 13. Known Gaps (V1) + +- Persistent retry RPC API formalization +- Event streaming API +- Topology-aware placement +- Federation controls +- Tenant-specific rate limiting and quotas + +## 14. Acceptance Criteria + +A V1 implementation is accepted when: + +- all workload actions are persisted before execution +- every scheduler->agent action is idempotent by `revision_id` +- reconciliation converges without hot-looping +- retry state survives restart +- node loss triggers deterministic reschedule flow +- all actions produce structured events diff --git a/persys-scheduler/docs/SCHEDULER_DESIGN_V1_IMPLEMENTATION_CHECKLIST.md b/persys-scheduler/docs/SCHEDULER_DESIGN_V1_IMPLEMENTATION_CHECKLIST.md new file mode 100644 index 0000000..efa1584 --- /dev/null +++ b/persys-scheduler/docs/SCHEDULER_DESIGN_V1_IMPLEMENTATION_CHECKLIST.md @@ -0,0 +1,155 @@ +# Scheduler Design V1 Implementation Checklist + +Source of truth reviewed: `docs/SCHEDULER_DESIGN_V1.md` + +Status legend: +- `Implemented`: behavior exists in code and is wired. +- `Partial`: implemented but not fully aligned with design requirements. +- `Missing`: not implemented. + +## Overall + +- Implemented: 30 +- Partial: 9 +- Missing: 6 +- Coverage estimate: ~67% fully implemented (30/45 checklist items) + +## 1. Purpose + +- [x] Implemented: Scheduler acts as control plane (placement + reconciliation + node management). Evidence: `internal/scheduler/scheduler.go`, `internal/scheduler/reconciler.go`, `internal/scheduler/node_control.go`. +- [x] Implemented: Scheduler does not run workloads directly; agent RPC is execution plane. Evidence: `internal/scheduler/agent_grpc.go`. + +## 2. Design Principles + +- [x] Implemented: Desired state persisted in etcd as primary source. Evidence: `internal/scheduler/state_store.go`, `internal/scheduler/workload_control.go`. +- [x] Implemented: Declarative desired state (`Running|Stopped|Deleted`) and reconcile loop converges. Evidence: `internal/scheduler/reconciler.go`. +- [~] Partial: Idempotency via `(workload_id, revision_id)` is passed to agent, but full enforcement depends on agent-side contract behavior. Evidence: `internal/scheduler/agent_grpc.go`, `internal/grpcapi/service.go`. +- [x] Implemented: Scheduler owns reconciliation logic. Evidence: `internal/scheduler/reconciler.go`. +- [x] Implemented: Deterministic decision/retry metadata persisted. Evidence: `/assignments`, `/reconciliation`, `/retries` in `internal/scheduler/state_store.go`. + +## 3. Architecture + +- [x] Implemented: Scheduler exposes gRPC control server and talks to agents over gRPC. Evidence: `cmd/scheduler/main.go`, `internal/grpcapi/service.go`, `internal/scheduler/agent_grpc.go`. +- [x] Implemented: mTLS mode exists with insecure test override. Evidence: `cmd/scheduler/main.go`. + +## 4.1 API Server + +- [x] Implemented: Request validation in control API methods. Evidence: `internal/grpcapi/service.go`. +- [x] Implemented: Desired state persisted before reconcile/apply flow. Evidence: `internal/grpcapi/service.go`, `internal/scheduler/workload_control.go`. +- [x] Implemented: Read APIs for inventory/status. Evidence: `ListNodes`, `GetNode`, `ListWorkloads`, `GetWorkload`, `GetClusterSummary` in `internal/grpcapi/service.go`. +- [~] Partial: Design lists REST endpoints; implementation is gRPC-only for control API. Evidence: `cmd/scheduler/main.go`, `internal/grpcapi/service.go`. + +## 4.2 State Store (etcd) + +- [x] Implemented: Key layout `/nodes`, `/workloads`, `/assignments`, `/reconciliation`, `/events`, `/retries`. Evidence: `internal/scheduler/state_store.go`. +- [~] Partial: Canonical workload record shape is close but not exact (`spec` is flattened into workload-specific fields). Evidence: `internal/models/models.go`, `internal/scheduler/workload_control.go`. + +## 4.3 Node Manager + +- [x] Implemented: Node registration. Evidence: `internal/grpcapi/service.go`, `internal/scheduler/scheduler.go`. +- [x] Implemented: Heartbeat updates node readiness and capacity fields. Evidence: `internal/grpcapi/service.go`, `internal/scheduler/node_control.go`. +- [x] Implemented: Missing heartbeat marks `NotReady`. Evidence: `internal/scheduler/scheduler.go` (`MonitorNodes`). +- [x] Implemented: Node loss triggers failover/reschedule path. Evidence: `internal/scheduler/reconciler.go` (`handleUnavailableAssignedNode`). +- [~] Partial: Node disk capacity accounting is not implemented (placeholder logic). Evidence: `internal/scheduler/scheduler.go` (`selectNodeForWorkload`). + +## 4.4 Placement Engine + +- [x] Implemented: Filters by readiness, CPU, memory, labels, workload type support. Evidence: `internal/scheduler/scheduler.go` (`selectNodeForWorkload`). +- [~] Partial: Disk filter required by design is not active. Evidence: `internal/scheduler/scheduler.go` (`availableDisk` placeholder). +- [x] Implemented: Sorts by utilization and picks first. Evidence: `internal/scheduler/scheduler.go` (`nodeUtilizationScore`, `sort.Slice`). +- [x] Implemented: Persists assignment with reason metadata. Evidence: `internal/scheduler/state_store.go` (`writeAssignment`), `internal/scheduler/scheduler.go` (`assignWorkload`). + +## 4.5 Reconciliation Engine + +- [x] Implemented: Periodic loop, configurable interval (default 5s). Evidence: `internal/scheduler/scheduler.go` (`StartReconciliation`), `internal/scheduler/reconciler.go`. +- [x] Implemented: Reconcile workflow uses desired state + agent actual state + action execution. Evidence: `internal/scheduler/reconciler.go`. +- [x] Implemented: Handles key transitions (`Running/Missing`, `Running/Failed`, `Stopped/Running`, `Deleted/*`). Evidence: `internal/scheduler/reconciler.go`. +- [x] Implemented: Transitional grace handling exists for missing state and unavailable nodes. Evidence: `internal/scheduler/reconciler.go` (`defaultMissingGracePeriod`, `defaultNodeUnavailableGrace`). +- [x] Implemented: Reconciliation metadata persisted per action. Evidence: `internal/scheduler/state_store.go` (`writeReconciliationRecord`). + +## 4.6 Retry Engine + +- [x] Implemented: Retry state persisted and updated on failures. Evidence: `internal/scheduler/workload_control.go`, `internal/scheduler/state_store.go`. +- [x] Implemented: Backoff schedule doubles from 5s and caps at 120s. Evidence: `internal/scheduler/workload_control.go` (`retryBackoff`). +- [x] Implemented: Stops at max attempts and marks workload failed. Evidence: `internal/scheduler/workload_control.go` (`UpdateWorkloadRetryOnFailure`). + +## 4.7 Event System + +- [x] Implemented: Immutable append-only events under `/events/`. Evidence: `internal/scheduler/state_store.go` (`emitEvent`). +- [x] Implemented: Event types used include `WorkloadScheduled`, `WorkloadFailed`, `NodeLost`, `RetryTriggered`, `Rescheduled`. Evidence: `internal/scheduler/*.go`. + +## 4.8 Automation Hooks + +- [ ] Missing: Deterministic rule engine for policy hooks (CPU threshold, repeated-failure alerts, webhooks) is not implemented. + +## 5. Workload Lifecycle + +- [x] Implemented: Create flow persists, assigns, reconciles, applies. Evidence: `internal/grpcapi/service.go` (`ApplyWorkload`), `internal/scheduler/workload_control.go`, `internal/scheduler/reconciler.go`. +- [x] Implemented: Update flow sets `Updating`, recomputes revision, reconciles. Evidence: `internal/scheduler/workload_control.go`, `internal/grpcapi/service.go`. +- [x] Implemented: Delete flow marks deleted, reconciler sends delete, and removes workload when agent returns not found. Evidence: `internal/scheduler/workload_control.go`, `internal/scheduler/reconciler.go`, `internal/scheduler/scheduler.go`. + +## 6. Failure Handling + +- [x] Implemented: Agent unreachable marks node `NotReady` and enters failover path. Evidence: `internal/scheduler/node_control.go`, `internal/scheduler/reconciler.go`. +- [x] Implemented: Retry policy with persisted reason and terminal failure. Evidence: `internal/scheduler/workload_control.go`. +- [x] Implemented: Heartbeat timeout detection and workload reschedule attempts. Evidence: `internal/scheduler/scheduler.go` (`MonitorNodes`), `internal/scheduler/reconciler.go`. + +## 7. Concurrency Model + +- [ ] Missing: Reconciliation worker pool is not implemented (loop is sequential per cycle). +- [x] Implemented: Context cancellation for long-running loops and shutdown. Evidence: `cmd/scheduler/main.go`, `internal/scheduler/reconciler.go`, `internal/scheduler/monitor.go`. +- [ ] Missing: Optimistic etcd concurrency (transaction compare-and-swap) is not implemented. +- [~] Partial: Global lock avoidance mostly true, but no explicit worker-parallelism controls. + +## 8. High Availability (Future) + +- [ ] Missing: Leader election and single active reconciler across replicas is not implemented. + +## 9. Security + +- [x] Implemented: mTLS for control plane/server and scheduler->agent path (with test insecure option). Evidence: `cmd/scheduler/main.go`, `internal/scheduler/agent_grpc.go`. +- [x] Implemented: CFSSL certificate issuance path exists. Evidence: `internal/auth/certmanager.go`. +- [~] Partial: Certificate rotation policy lifecycle is basic (`EnsureCertificate`) and not fully operationalized/documented. +- [ ] Missing: RBAC enforcement is not implemented in scheduler service (expected at API gateway). + +## 10. Observability + +- [x] Implemented: `/metrics` and `/health` HTTP endpoints. Evidence: `cmd/scheduler/main.go`. +- [ ] Missing: Design-named core scheduler counters are not implemented (`scheduling_attempts_total`, `reconciliation_actions_total`, `retry_total`, `node_unhealthy_total`). + +## 11. Scale Targets + +- [~] Partial: Architecture can handle moderate scale, but no explicit load/scale validation or capacity tests in repo. + +## 12. Out of Scope + +- [x] Implemented: Scheduler does not build images. +- [x] Implemented: Scheduler does not execute runtimes directly. +- [x] Implemented: Scheduler does not store centralized full runtime logs backend (only per-workload status/log snippets in etcd model). +- [x] Implemented: Agent-side reconciliation is not used as cluster control mechanism. + +## 13. Known Gaps (V1) + +- [x] Implemented as known gap: Event streaming API still missing. +- [x] Implemented as known gap: Topology-aware placement still missing. +- [x] Implemented as known gap: Federation controls still missing. +- [x] Implemented as known gap: Tenant quotas/rate-limits still missing. +- [~] Partial: Retry RPC exists (`RetryWorkload`) but persistent retry API formalization/versioning is still basic. + +## 14. Acceptance Criteria Check + +- [x] Implemented: Workload state persisted before runtime action execution. +- [~] Partial: Scheduler->agent idempotency by `revision_id` is wired, but full guarantee depends on strict agent enforcement. +- [x] Implemented: Reconciliation avoids hot-loop behavior through interval + retry windows. +- [x] Implemented: Retry state survives restart via persisted workload retry fields and `/retries` key. +- [x] Implemented: Node loss triggers deterministic reschedule/failover path. +- [~] Partial: Most actions emit structured events, but metric/event coverage is not yet complete for all observability requirements. + +## Priority Gap List (Recommended Next) + +- [ ] Implement disk-aware scheduling filter and node disk accounting. +- [ ] Add reconciliation worker pool with bounded concurrency. +- [ ] Add optimistic etcd writes (`Txn` compare-and-swap) for conflict-safe updates. +- [ ] Implement core Prometheus counters listed in design. +- [ ] Implement automation hooks/rule engine. +- [ ] Define/implement leader election for HA reconciler ownership. diff --git a/persys-scheduler/docs/SCHEDULER_RECONCILIATION_AND_SCHEDULING_DEEP_DIVE.md b/persys-scheduler/docs/SCHEDULER_RECONCILIATION_AND_SCHEDULING_DEEP_DIVE.md new file mode 100644 index 0000000..6e34e23 --- /dev/null +++ b/persys-scheduler/docs/SCHEDULER_RECONCILIATION_AND_SCHEDULING_DEEP_DIVE.md @@ -0,0 +1,274 @@ +# Persys Scheduler Reconciliation and Scheduling Deep Dive + +## Scope + +This document describes the **current implemented behavior** of scheduling and reconciliation in `persys-scheduler`. + +It covers: + +- control-plane APIs and state writes +- node registration and health lifecycle +- placement decisions +- reconciliation loop decisions and actions +- failover and retry behavior +- runtime mismatch handling +- known implementation gaps + +## Components and Responsibilities + +- `internal/grpcapi/service.go` + - Accepts control-plane RPCs (`RegisterNode`, `Heartbeat`, `ApplyWorkload`, `DeleteWorkload`, `RetryWorkload`) + - Converts API payloads to internal models + - Persists desired state via scheduler services + - Triggers immediate reconciliation on `ApplyWorkload` +- `internal/scheduler/scheduler.go` + - Node/workload CRUD against etcd + - Node selection and assignment + - Node health monitor loop + - Reconciliation loop startup +- `internal/scheduler/reconciler.go` + - Compares desired vs actual state + - Handles apply/stop/delete decisions + - Handles failover and runtime mismatch recovery +- `internal/scheduler/agent_grpc.go` + - Scheduler -> Agent gRPC client (apply/delete/status/actions) + - Dial policy and error classification +- `internal/scheduler/node_control.go` + - Heartbeat updates + - NotReady transitions and status provenance fields + +## etcd State Layout + +State keys: + +- `/nodes/` +- `/nodes//status` +- `/workloads/` +- `/assignments/` +- `/reconciliation/` +- `/retries/` +- `/events/` + +Where mapping lives: + +- Primary: workload record fields `nodeId` and `assignedNode` +- Index: assignment record in `/assignments/` + +## Node Lifecycle + +### Registration + +RPC: `RegisterNode` + +Behavior: + +- Requires `node_id` and now requires valid `grpc_endpoint` (`host:port`) +- Stores endpoint in node model as `agentEndpoint` +- Stores capabilities including `supported_workload_types` +- Initializes status metadata: + - `status=Ready` + - `statusReason=registered` + - `statusUpdatedBy=register` + - `statusUpdatedAt=` +- Persists node and `/status` shadow key + +### Heartbeat + +RPC: `Heartbeat` + +Behavior: + +- Updates `lastHeartbeat` +- Sets status to `Ready` +- Updates node availability from usage (`total - used`) +- Updates status provenance on transition/recovery: + - `statusReason=heartbeat received` or transition string + - `statusUpdatedBy=heartbeat` + - `statusUpdatedAt=` + +### Node health monitor + +Loop: every 1 minute in `MonitorNodes` + +Behavior: + +- If `time.Since(lastHeartbeat) > 3m`, marks node `NotReady` +- Writes status provenance: + - `statusReason=heartbeat expired: last heartbeat ` + - `statusUpdatedBy=monitor` + - `statusUpdatedAt=` + +## Scheduling Logic + +Entry points: + +- Immediate path: `ApplyWorkload` RPC calls `Create/Update` then `ReconcileWorkload` +- Periodic path: reconciliation loop every `SCHEDULER_RECONCILE_INTERVAL` (default 5s) + +### Placement (`selectNodeForWorkload`) + +Node filters in order: + +- Skip `/status` subkeys +- `status` must be `Ready` or `Active` +- heartbeat freshness <= 10 minutes +- labels must match workload labels +- node must support workload type + - Uses `supportedWorkloadTypes` if present + - VM strict fallback for legacy records: requires hypervisor signal +- CPU availability +- memory availability +- disk check currently placeholder (not enforced) + +Selection: + +- Sort by lowest utilization score: average of CPU and memory used ratios +- Pick first candidate + +If no candidate: + +- returns detailed rejection reasons per node + +### Assignment (`assignWorkload`) + +Effects: + +- Sets workload `nodeId` and `assignedNode` +- Status becomes `Scheduled`/`Pending` +- Writes workload record +- Writes `/assignments/` +- Emits `WorkloadScheduled` event + +## Reconciliation Logic + +Loop: + +- Reads all workloads from etcd +- Skips `Completed` and `Deleted` status entries +- Reconciles each workload independently + +### Per-workload flow (`ReconcileWorkload`) + +1. Respect retry backoff + +- If `nextRetryAt` is in future, action is `BackoffWait` + +2. Finalize deletion when desired `Deleted` and no assigned node + +3. Ensure assignment exists (unless deleting) + +- If unassigned, run placement and assign + +4. Retry due handling + +- If retry timer elapsed, clear `nextRetryAt`, set workload `Pending` + +5. Assigned node availability pre-check + +- If assigned node is unavailable (`NotReady/!Active` or stale heartbeat), attempt failover + +6. Fetch actual runtime state from agent (`GetWorkloadStatus`) + +- `NotFound`-class errors map to `Missing` +- Unreachable errors (dial/DNS/unavailable) trigger: + - mark assigned node `NotReady` + - failover attempt in same cycle + +7. Compare desired vs actual + +- Desired `Running` and actual in `{Missing,Stopped,Failed,Unknown}` -> apply running +- Desired `Stopped` and actual in `{Running,Pending,Unknown}` -> apply stopped +- Desired `Deleted` -> delete path +- Otherwise no action + +8. Persist reconciliation metadata + +- Writes `/reconciliation/` +- Updates workload metadata fields (`lastReconciliation*`) + +### Missing/pending grace + +For actual `{Missing,Pending}`, reconciliation can defer action briefly if `lastLaunchTime` is recent. + +- Config: `SCHEDULER_MISSING_GRACE_PERIOD` +- Default: 15s + +## Retry Engine + +On failures via `UpdateWorkloadRetryOnFailure`: + +- `attempts++` +- backoff: 5s, 10s, 20s, 40s, ... capped at 2m +- when `attempts >= maxAttempts`: + - mark workload `Failed` + - stop retrying + +Manual retry: + +- `RetryWorkload` sets `nextRetryAt=now` + +## Failover Behavior + +If assigned node unavailable: + +- select another eligible node +- if none available: + - workload marked for retry backoff (`AwaitFailover` path) +- if new node selected: + - workload reassigned + - reschedule event/log written + +## Runtime Mismatch Recovery + +If apply fails with `runtime not available`: + +- scheduler marks assigned node as unsupported for that workload type +- clears workload assignment (`nodeId`/`assignedNode`) +- deletes stale `/assignments/` index +- workload status set to `Pending` for reschedule +- emits reschedule event + +## Scheduler -> Agent Routing + +Dial target preference: + +- First: `node.agentEndpoint` (exact registered `grpc_endpoint`) +- Fallback: `node.ipAddress:agentGrpcPort` + +Implication: + +- Use routable `grpc_endpoint` (prefer explicit IP) to avoid DNS surprises + +## Observability Behavior + +Node status provenance fields: + +- `statusReason` +- `statusUpdatedBy` +- `statusUpdatedAt` + +Reconciliation loop logging: + +- Logs cycle summary only when there are actions or failures + +Placement rejection logs include exact per-node reason. + +## Known Gaps (Current) + +- Disk capacity filter is placeholder and not enforcing real disk constraints +- Workload update paths use read-modify-write without etcd CAS; concurrent updates can overwrite fields +- No stream of reconciliation decisions; mostly log/event + etcd inspection +- No tenant quota/rate-limit scheduling policy + +## Operational Recommendations + +- Ensure every agent sends heartbeat continuously (not just register) +- Register truthful `supported_workload_types` + - VM-only nodes: include `vm` + - non-VM nodes: do not include `vm` +- Always register routable `grpc_endpoint` values +- Monitor: + - node status transitions (`statusReason/by/at`) + - failover pending reasons + - workload retry counters and terminal failures + diff --git a/persys-scheduler/go.mod b/persys-scheduler/go.mod new file mode 100644 index 0000000..45f6729 --- /dev/null +++ b/persys-scheduler/go.mod @@ -0,0 +1,54 @@ +module github.com/persys-dev/persys-cloud/persys-scheduler + +go 1.24.1 + +require ( + github.com/google/uuid v1.6.0 + github.com/prometheus/client_golang v1.11.1 + github.com/sirupsen/logrus v1.6.0 + go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.65.0 + go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.46.1 + go.opentelemetry.io/otel v1.40.0 + go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.40.0 + go.opentelemetry.io/otel/sdk v1.40.0 + go.opentelemetry.io/otel/trace v1.40.0 + google.golang.org/grpc v1.78.0 +) + +require ( + github.com/beorn7/perks v1.0.1 // indirect + github.com/cenkalti/backoff/v5 v5.0.3 // indirect + github.com/cespare/xxhash/v2 v2.3.0 // indirect + github.com/coreos/go-semver v0.3.0 // indirect + github.com/coreos/go-systemd/v22 v22.3.2 // indirect + github.com/felixge/httpsnoop v1.0.4 // indirect + github.com/go-logr/logr v1.4.3 // indirect + github.com/go-logr/stdr v1.2.2 // indirect + github.com/gogo/protobuf v1.3.2 // indirect + github.com/golang/protobuf v1.5.4 // indirect + github.com/grpc-ecosystem/grpc-gateway/v2 v2.27.7 // indirect + github.com/konsorten/go-windows-terminal-sequences v1.0.3 // indirect + github.com/matttproud/golang_protobuf_extensions v1.0.1 // indirect + github.com/prometheus/client_model v0.2.0 // indirect + github.com/prometheus/common v0.26.0 // indirect + github.com/prometheus/procfs v0.6.0 // indirect + go.etcd.io/etcd/api/v3 v3.5.21 // indirect + go.etcd.io/etcd/client/pkg/v3 v3.5.21 // indirect + go.opentelemetry.io/auto/sdk v1.2.1 // indirect + go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.40.0 // indirect + go.opentelemetry.io/otel/metric v1.40.0 // indirect + go.opentelemetry.io/proto/otlp v1.9.0 // indirect + go.uber.org/atomic v1.7.0 // indirect + go.uber.org/multierr v1.6.0 // indirect + go.uber.org/zap v1.17.0 // indirect + google.golang.org/genproto/googleapis/api v0.0.0-20260128011058-8636f8732409 // indirect + google.golang.org/genproto/googleapis/rpc v0.0.0-20260128011058-8636f8732409 // indirect +) + +require ( + go.etcd.io/etcd/client/v3 v3.5.21 + golang.org/x/net v0.49.0 // indirect + golang.org/x/sys v0.40.0 // indirect + golang.org/x/text v0.33.0 // indirect + google.golang.org/protobuf v1.36.11 +) diff --git a/prow/go.sum b/persys-scheduler/go.sum similarity index 66% rename from prow/go.sum rename to persys-scheduler/go.sum index 1ced59a..53cda9d 100644 --- a/prow/go.sum +++ b/persys-scheduler/go.sum @@ -8,19 +8,11 @@ github.com/beorn7/perks v0.0.0-20180321164747-3a771d992973/go.mod h1:Dwedo/Wpr24 github.com/beorn7/perks v1.0.0/go.mod h1:KWe93zE9D1o94FZ5RNwFwVgaQK1VOXiVxmqh+CedLV8= github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM= github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw= -github.com/bytedance/sonic v1.13.2 h1:8/H1FempDZqC4VqjptGo14QQlJx8VdZJegxs6wwfqpQ= -github.com/bytedance/sonic v1.13.2/go.mod h1:o68xyaF9u2gvVBuGHPlUVCy+ZfmNNO5ETf1+KgkJhz4= -github.com/bytedance/sonic/loader v0.1.1/go.mod h1:ncP89zfokxS5LZrJxl5z0UJcsk4M4yY2JpfqGeCtNLU= -github.com/bytedance/sonic/loader v0.2.4 h1:ZWCw4stuXUsn1/+zQDqeE7JKP+QO47tz7QCNan80NzY= -github.com/bytedance/sonic/loader v0.2.4/go.mod h1:N8A3vUdtUebEY2/VQC0MyhYeKUFosQU6FxH2JmUe6VI= -github.com/cenkalti/backoff/v5 v5.0.2 h1:rIfFVxEf1QsI7E1ZHfp/B4DF/6QBAUhmgkxc0H7Zss8= -github.com/cenkalti/backoff/v5 v5.0.2/go.mod h1:rkhZdG3JZukswDf7f0cwqPNk4K0sa+F97BxZthm/crw= +github.com/cenkalti/backoff/v5 v5.0.3 h1:ZN+IMa753KfX5hd8vVaMixjnqRZ3y8CuJKRKj1xcsSM= +github.com/cenkalti/backoff/v5 v5.0.3/go.mod h1:rkhZdG3JZukswDf7f0cwqPNk4K0sa+F97BxZthm/crw= github.com/cespare/xxhash/v2 v2.1.1/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs= github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= -github.com/cloudwego/base64x v0.1.5 h1:XPciSp1xaq2VCSt6lF0phncD4koWyULpl5bUxbfCyP4= -github.com/cloudwego/base64x v0.1.5/go.mod h1:0zlkT4Wn5C6NdauXdJRhSKRlJvmclQ1hhJgA0rcu/8w= -github.com/cloudwego/iasm v0.2.0/go.mod h1:8rXZaNYT2n95jn+zTI1sDr+IgcD2GVs0nlbbQPiEFhY= github.com/coreos/go-semver v0.3.0 h1:wkHLiw0WNATZnSG7epLsujiMCgPAc9xhjJ4tgnAxmfM= github.com/coreos/go-semver v0.3.0/go.mod h1:nnelYz7RCh+5ahJtPPxZlU+153eP4D4r3EedlOD2RNk= github.com/coreos/go-systemd/v22 v22.3.2 h1:D9/bQk5vlXQFZ6Kwuu6zaiXJ9oTPe68++AzAJc1DzSI= @@ -30,12 +22,6 @@ github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/felixge/httpsnoop v1.0.4 h1:NFTV2Zj1bL4mc9sqWACXbQFVBBg2W3GPvqp8/ESS2Wg= github.com/felixge/httpsnoop v1.0.4/go.mod h1:m8KPJKqk1gH5J9DgRY2ASl2lWCfGKXixSwevea8zH2U= -github.com/gabriel-vasile/mimetype v1.4.9 h1:5k+WDwEsD9eTLL8Tz3L0VnmVh9QxGjRmjBvAG7U/oYY= -github.com/gabriel-vasile/mimetype v1.4.9/go.mod h1:WnSQhFKJuBlRyLiKohA/2DtIlPFAbguNaG7QCHcyGok= -github.com/gin-contrib/sse v1.1.0 h1:n0w2GMuUpWDVp7qSpvze6fAu9iRxJY4Hmj6AmBOU05w= -github.com/gin-contrib/sse v1.1.0/go.mod h1:hxRZ5gVpWMT7Z0B0gSNYqqsSCNIJMjzvm6fqCz9vjwM= -github.com/gin-gonic/gin v1.10.1 h1:T0ujvqyCSqRopADpgPgiTT63DUQVSfojyME59Ei63pQ= -github.com/gin-gonic/gin v1.10.1/go.mod h1:4PMNQiOhvDRa013RKVbsiNwoyezlm2rm0uX/T7kzp5Y= github.com/go-kit/kit v0.8.0/go.mod h1:xBxKIO96dXMWWy0MnWVtmwkA9/13aqxPnvrjFYMA2as= github.com/go-kit/kit v0.9.0/go.mod h1:xBxKIO96dXMWWy0MnWVtmwkA9/13aqxPnvrjFYMA2as= github.com/go-kit/log v0.1.0/go.mod h1:zbhenjAZHb184qTLMA9ZjW7ThYL0H2mk7Q6pNt4vbaY= @@ -43,21 +29,11 @@ github.com/go-logfmt/logfmt v0.3.0/go.mod h1:Qt1PoO58o5twSAckw1HlFXLmHsOX5/0LbT9 github.com/go-logfmt/logfmt v0.4.0/go.mod h1:3RMwSq7FuexP4Kalkev3ejPJsZTpXXBr9+V4qmtdjCk= github.com/go-logfmt/logfmt v0.5.0/go.mod h1:wCYkCAKZfumFQihp8CzCvQ3paCTfi41vtzG1KdI/P7A= github.com/go-logr/logr v1.2.2/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A= -github.com/go-logr/logr v1.4.2 h1:6pFjapn8bFcIbiKo3XT4j/BhANplGihG6tvd+8rYgrY= -github.com/go-logr/logr v1.4.2/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY= +github.com/go-logr/logr v1.4.3 h1:CjnDlHq8ikf6E492q6eKboGOC0T8CDaOvkHCIg8idEI= +github.com/go-logr/logr v1.4.3/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY= github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag= github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE= -github.com/go-playground/assert/v2 v2.2.0 h1:JvknZsQTYeFEAhQwI4qEt9cyV5ONwRHC+lYKSsYSR8s= -github.com/go-playground/assert/v2 v2.2.0/go.mod h1:VDjEfimB/XKnb+ZQfWdccd7VUvScMdVu0Titje2rxJ4= -github.com/go-playground/locales v0.14.1 h1:EWaQ/wswjilfKLTECiXz7Rh+3BjFhfDFKv/oXslEjJA= -github.com/go-playground/locales v0.14.1/go.mod h1:hxrqLVvrK65+Rwrd5Fc6F2O76J/NuW9t0sjnWqG1slY= -github.com/go-playground/universal-translator v0.18.1 h1:Bcnm0ZwsGyWbCzImXv+pAJnYK9S473LQFuzCbDbfSFY= -github.com/go-playground/universal-translator v0.18.1/go.mod h1:xekY+UJKNuX9WP91TpwSH2VMlDf28Uj24BCp08ZFTUY= -github.com/go-playground/validator/v10 v10.26.0 h1:SP05Nqhjcvz81uJaRfEV0YBSSSGMc/iMaVtFbr3Sw2k= -github.com/go-playground/validator/v10 v10.26.0/go.mod h1:I5QpIEbmr8On7W0TktmJAumgzX4CA1XNl4ZmDuVHKKo= github.com/go-stack/stack v1.8.0/go.mod h1:v0f6uXyyMGvRgIKkXu+yp6POWl0qKG85gN/melR3HDY= -github.com/goccy/go-json v0.10.5 h1:Fq85nIqj+gXn/S5ahsiTlK3TmC85qgirsdTP/+DeaC4= -github.com/goccy/go-json v0.10.5/go.mod h1:oq7eo15ShAhp70Anwd5lgX2pLfOS3QCiwU/PULtXL6M= github.com/godbus/dbus/v5 v5.0.4/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA= github.com/gogo/protobuf v1.1.1/go.mod h1:r8qH/GZQm5c6nD/R0oafs1akxWv10x8SbQlK7atdtwQ= github.com/gogo/protobuf v1.3.2 h1:Ov1cvc58UF3b5XjBnZv7+opcTcQFZebYjWzi34vdm4Q= @@ -84,50 +60,31 @@ github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= -github.com/grpc-ecosystem/grpc-gateway/v2 v2.26.3 h1:5ZPtiqj0JL5oKWmcsq4VMaAW5ukBEgSGXEN89zeH1Jo= -github.com/grpc-ecosystem/grpc-gateway/v2 v2.26.3/go.mod h1:ndYquD05frm2vACXE1nsccT4oJzjhw2arTS2cpUD1PI= +github.com/grpc-ecosystem/grpc-gateway/v2 v2.27.7 h1:X+2YciYSxvMQK0UZ7sg45ZVabVZBeBuvMkmuI2V3Fak= +github.com/grpc-ecosystem/grpc-gateway/v2 v2.27.7/go.mod h1:lW34nIZuQ8UDPdkon5fmfp2l3+ZkQ2me/+oecHYLOII= github.com/jpillora/backoff v1.0.0/go.mod h1:J/6gKK9jxlEcS3zixgDgUAsiuZ7yrSoa/FX5e0EB2j4= github.com/json-iterator/go v1.1.6/go.mod h1:+SdeFBvtyEkXs7REEP0seUULqWtbJapLOCVDaaPEHmU= github.com/json-iterator/go v1.1.10/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4= github.com/json-iterator/go v1.1.11/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4= -github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM= -github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo= github.com/julienschmidt/httprouter v1.2.0/go.mod h1:SYymIcj16QtmaHHD7aYtjjsJG7VTCxuUUipMqKk8s4w= github.com/julienschmidt/httprouter v1.3.0/go.mod h1:JR6WtHb+2LUe8TCKY3cZOxFyyO8IZAc4RVcycCCAKdM= github.com/kisielk/errcheck v1.5.0/go.mod h1:pFxgyoBC7bSaBwPgfKdkLd5X25qrDl4LWUI2bnpBCr8= github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck= -github.com/klauspost/cpuid/v2 v2.0.9/go.mod h1:FInQzS24/EEf25PyTYn52gqo7WaD8xa0213Md/qVLRg= -github.com/klauspost/cpuid/v2 v2.2.10 h1:tBs3QSyvjDyFTq3uoc/9xFpCuOsJQFNPiAhYdw2skhE= -github.com/klauspost/cpuid/v2 v2.2.10/go.mod h1:hqwkgyIinND0mEev00jJYCxPNVRVXFQeu1XKlok6oO0= -github.com/knz/go-libedit v1.10.1/go.mod h1:MZTVkCWyz0oBc7JOWP3wNAzd002ZbM/5hgShxwh4x8M= github.com/konsorten/go-windows-terminal-sequences v1.0.1/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ= github.com/konsorten/go-windows-terminal-sequences v1.0.3 h1:CE8S1cTafDpPvMhIxNJKvHsGVBgn1xWYf1NbHQhywc8= github.com/konsorten/go-windows-terminal-sequences v1.0.3/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ= github.com/kr/logfmt v0.0.0-20140226030751-b84e30acd515/go.mod h1:+0opPa2QZZtGFBFZlji/RkVcI2GknAs/DXo4wKdlNEc= github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= -github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE= -github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk= github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= -github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= -github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= -github.com/leodido/go-urn v1.4.0 h1:WT9HwE9SGECu3lg4d/dIA+jxlljEa1/ffXKmRjqdmIQ= -github.com/leodido/go-urn v1.4.0/go.mod h1:bvxc+MVxLKB4z00jd1z+Dvzr47oO32F/QSNjSBOlFxI= -github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY= -github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= github.com/matttproud/golang_protobuf_extensions v1.0.1 h1:4hp9jkHxhMHkqkrB3Ix0jegS5sx/RkqARlsWZ6pIwiU= github.com/matttproud/golang_protobuf_extensions v1.0.1/go.mod h1:D8He9yQNgCq6Z5Ld7szi9bcBfOoFv/3dc6xSMkL2PC0= github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= -github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg= github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= github.com/modern-go/reflect2 v0.0.0-20180701023420-4b7aa43c6742/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0= github.com/modern-go/reflect2 v1.0.1/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0= -github.com/modern-go/reflect2 v1.0.2 h1:xBagoLtFs94CBntxluKeaWgTMpvLxC4ur3nMaC9Gz0M= -github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk= github.com/mwitkow/go-conntrack v0.0.0-20161129095857-cc309e4a2223/go.mod h1:qRWi+5nqEBWmkhHvq77mSJWrCKwh8bxhgT7d/eI7P4U= github.com/mwitkow/go-conntrack v0.0.0-20190716064945-2f068394615f/go.mod h1:qRWi+5nqEBWmkhHvq77mSJWrCKwh8bxhgT7d/eI7P4U= -github.com/pelletier/go-toml/v2 v2.2.4 h1:mye9XuhQ6gvn5h28+VilKrrPoQVanw5PMw/TB0t5Ec4= -github.com/pelletier/go-toml/v2 v2.2.4/go.mod h1:2gIqNv+qfxSVS7cM2xJQKtLSTLUE9V8t9Stt+h56mCY= github.com/pkg/errors v0.8.0/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= @@ -152,63 +109,48 @@ github.com/prometheus/procfs v0.0.2/go.mod h1:TjEm7ze935MbeOT/UhFTIMYKhuLP4wbCsT github.com/prometheus/procfs v0.1.3/go.mod h1:lV6e/gmhEcM9IjHGsFOCxxuZ+z1YqCvr4OA4YeYWdaU= github.com/prometheus/procfs v0.6.0 h1:mxy4L2jP6qMonqmq+aTtOx1ifVWUgG/TAmntgbh3xv4= github.com/prometheus/procfs v0.6.0/go.mod h1:cz+aTbrPOrUb4q7XlbU9ygM+/jj0fzG6c1xBZuNvfVA= -github.com/rogpeppe/go-internal v1.13.1 h1:KvO1DLK/DRN07sQ1LQKScxyZJuNnedQ5/wKSR38lUII= -github.com/rogpeppe/go-internal v1.13.1/go.mod h1:uMEvuHeurkdAXX61udpOXGD/AzZDWNMNyH2VO9fmH0o= github.com/sirupsen/logrus v1.2.0/go.mod h1:LxeOpSwHxABJmUn/MG1IvRgCAasNZTLOkJPxbbu5VWo= github.com/sirupsen/logrus v1.4.2/go.mod h1:tLMulIdttU9McNUspp0xgXVQah82FyeX6MwdIuYE2rE= github.com/sirupsen/logrus v1.6.0 h1:UBcNElsrwanuuMsnGSlYmtmgbb23qDR5dG+6X6Oo89I= github.com/sirupsen/logrus v1.6.0/go.mod h1:7uNnSEd1DgxDLC74fIahvMZmmYsHGZGEOFrfsX/uA88= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= github.com/stretchr/objx v0.1.1/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= -github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw= -github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo= github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4= github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= -github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= -github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= -github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4= -github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA= -github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= -github.com/twitchyliquid64/golang-asm v0.15.1 h1:SU5vSMR7hnwNxj24w34ZyCi/FmDZTkS4MhqMhdFk5YI= -github.com/twitchyliquid64/golang-asm v0.15.1/go.mod h1:a1lVb/DtPvCB8fslRZhAngC2+aY1QWCk3Cedj/Gdt08= -github.com/ugorji/go/codec v1.2.12 h1:9LC83zGrHhuUA9l16C9AHXAqEV/2wBQ4nkvumAE65EE= -github.com/ugorji/go/codec v1.2.12/go.mod h1:UNopzCgEMSXjBc6AOMqYvWC1ktqTAfzJZUZgYf6w6lg= +github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U= +github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U= github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= -github.com/zsais/go-gin-prometheus v0.1.0 h1:bkLv1XCdzqVgQ36ScgRi09MA2UC1t3tAB6nsfErsGO4= -github.com/zsais/go-gin-prometheus v0.1.0/go.mod h1:Slirjzuz8uM8Cw0jmPNqbneoqcUtY2GGjn2bEd4NRLY= go.etcd.io/etcd/api/v3 v3.5.21 h1:A6O2/JDb3tvHhiIz3xf9nJ7REHvtEFJJ3veW3FbCnS8= go.etcd.io/etcd/api/v3 v3.5.21/go.mod h1:c3aH5wcvXv/9dqIw2Y810LDXJfhSYdHQ0vxmP3CCHVY= go.etcd.io/etcd/client/pkg/v3 v3.5.21 h1:lPBu71Y7osQmzlflM9OfeIV2JlmpBjqBNlLtcoBqUTc= go.etcd.io/etcd/client/pkg/v3 v3.5.21/go.mod h1:BgqT/IXPjK9NkeSDjbzwsHySX3yIle2+ndz28nVsjUs= go.etcd.io/etcd/client/v3 v3.5.21 h1:T6b1Ow6fNjOLOtM0xSoKNQt1ASPCLWrF9XMHcH9pEyY= go.etcd.io/etcd/client/v3 v3.5.21/go.mod h1:mFYy67IOqmbRf/kRUvsHixzo3iG+1OF2W2+jVIQRAnU= -go.opentelemetry.io/auto/sdk v1.1.0 h1:cH53jehLUN6UFLY71z+NDOiNJqDdPRaXzTel0sJySYA= -go.opentelemetry.io/auto/sdk v1.1.0/go.mod h1:3wSPjt5PWp2RhlCcmmOial7AvC4DQqZb7a7wCow3W8A= -go.opentelemetry.io/contrib/instrumentation/github.com/gin-gonic/gin/otelgin v0.61.0 h1:VkrF0D14uQrCmPqBkYlwWnhgcwzXvIRAjX8eXO7vy6M= -go.opentelemetry.io/contrib/instrumentation/github.com/gin-gonic/gin/otelgin v0.61.0/go.mod h1:p/mVr/Hs7gQnguNPXUyuiMRNtisyc9y/Oo7Kqr/6wbU= +go.opentelemetry.io/auto/sdk v1.2.1 h1:jXsnJ4Lmnqd11kwkBV2LgLoFMZKizbCi5fNZ/ipaZ64= +go.opentelemetry.io/auto/sdk v1.2.1/go.mod h1:KRTj+aOaElaLi+wW1kO/DZRXwkF4C5xPbEe3ZiIhN7Y= +go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.65.0 h1:XmiuHzgJt067+a6kwyAzkhXooYVv3/TOw9cM2VfJgUM= +go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.65.0/go.mod h1:KDgtbWKTQs4bM+VPUr6WlL9m/WXcmkCcBlIzqxPGzmI= go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.46.1 h1:aFJWCqJMNjENlcleuuOkGAPH82y0yULBScfXcIEdS24= go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.46.1/go.mod h1:sEGXWArGqc3tVa+ekntsN65DmVbVeW+7lTKTjZF3/Fo= -go.opentelemetry.io/otel v1.36.0 h1:UumtzIklRBY6cI/lllNZlALOF5nNIzJVb16APdvgTXg= -go.opentelemetry.io/otel v1.36.0/go.mod h1:/TcFMXYjyRNh8khOAO9ybYkqaDBb/70aVwkNML4pP8E= -go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.36.0 h1:dNzwXjZKpMpE2JhmO+9HsPl42NIXFIFSUSSs0fiqra0= -go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.36.0/go.mod h1:90PoxvaEB5n6AOdZvi+yWJQoE95U8Dhhw2bSyRqnTD0= -go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.36.0 h1:nRVXXvf78e00EwY6Wp0YII8ww2JVWshZ20HfTlE11AM= -go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.36.0/go.mod h1:r49hO7CgrxY9Voaj3Xe8pANWtr0Oq916d0XAmOoCZAQ= -go.opentelemetry.io/otel/exporters/stdout/stdouttrace v1.36.0 h1:G8Xec/SgZQricwWBJF/mHZc7A02YHedfFDENwJEdRA0= -go.opentelemetry.io/otel/exporters/stdout/stdouttrace v1.36.0/go.mod h1:PD57idA/AiFD5aqoxGxCvT/ILJPeHy3MjqU/NS7KogY= -go.opentelemetry.io/otel/metric v1.36.0 h1:MoWPKVhQvJ+eeXWHFBOPoBOi20jh6Iq2CcCREuTYufE= -go.opentelemetry.io/otel/metric v1.36.0/go.mod h1:zC7Ks+yeyJt4xig9DEw9kuUFe5C3zLbVjV2PzT6qzbs= -go.opentelemetry.io/otel/sdk v1.36.0 h1:b6SYIuLRs88ztox4EyrvRti80uXIFy+Sqzoh9kFULbs= -go.opentelemetry.io/otel/sdk v1.36.0/go.mod h1:+lC+mTgD+MUWfjJubi2vvXWcVxyr9rmlshZni72pXeY= -go.opentelemetry.io/otel/sdk/metric v1.34.0 h1:5CeK9ujjbFVL5c1PhLuStg1wxA7vQv7ce1EK0Gyvahk= -go.opentelemetry.io/otel/sdk/metric v1.34.0/go.mod h1:jQ/r8Ze28zRKoNRdkjCZxfs6YvBTG1+YIqyFVFYec5w= -go.opentelemetry.io/otel/trace v1.36.0 h1:ahxWNuqZjpdiFAyrIoQ4GIiAIhxAunQR6MUoKrsNd4w= -go.opentelemetry.io/otel/trace v1.36.0/go.mod h1:gQ+OnDZzrybY4k4seLzPAWNwVBBVlF2szhehOBB/tGA= -go.opentelemetry.io/proto/otlp v1.6.0 h1:jQjP+AQyTf+Fe7OKj/MfkDrmK4MNVtw2NpXsf9fefDI= -go.opentelemetry.io/proto/otlp v1.6.0/go.mod h1:cicgGehlFuNdgZkcALOCh3VE6K/u2tAjzlRhDwmVpZc= +go.opentelemetry.io/otel v1.40.0 h1:oA5YeOcpRTXq6NN7frwmwFR0Cn3RhTVZvXsP4duvCms= +go.opentelemetry.io/otel v1.40.0/go.mod h1:IMb+uXZUKkMXdPddhwAHm6UfOwJyh4ct1ybIlV14J0g= +go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.40.0 h1:QKdN8ly8zEMrByybbQgv8cWBcdAarwmIPZ6FThrWXJs= +go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.40.0/go.mod h1:bTdK1nhqF76qiPoCCdyFIV+N/sRHYXYCTQc+3VCi3MI= +go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.40.0 h1:wVZXIWjQSeSmMoxF74LzAnpVQOAFDo3pPji9Y4SOFKc= +go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.40.0/go.mod h1:khvBS2IggMFNwZK/6lEeHg/W57h/IX6J4URh57fuI40= +go.opentelemetry.io/otel/metric v1.40.0 h1:rcZe317KPftE2rstWIBitCdVp89A2HqjkxR3c11+p9g= +go.opentelemetry.io/otel/metric v1.40.0/go.mod h1:ib/crwQH7N3r5kfiBZQbwrTge743UDc7DTFVZrrXnqc= +go.opentelemetry.io/otel/sdk v1.40.0 h1:KHW/jUzgo6wsPh9At46+h4upjtccTmuZCFAc9OJ71f8= +go.opentelemetry.io/otel/sdk v1.40.0/go.mod h1:Ph7EFdYvxq72Y8Li9q8KebuYUr2KoeyHx0DRMKrYBUE= +go.opentelemetry.io/otel/sdk/metric v1.40.0 h1:mtmdVqgQkeRxHgRv4qhyJduP3fYJRMX4AtAlbuWdCYw= +go.opentelemetry.io/otel/sdk/metric v1.40.0/go.mod h1:4Z2bGMf0KSK3uRjlczMOeMhKU2rhUqdWNoKcYrtcBPg= +go.opentelemetry.io/otel/trace v1.40.0 h1:WA4etStDttCSYuhwvEa8OP8I5EWu24lkOzp+ZYblVjw= +go.opentelemetry.io/otel/trace v1.40.0/go.mod h1:zeAhriXecNGP/s2SEG3+Y8X9ujcJOTqQ5RgdEJcawiA= +go.opentelemetry.io/proto/otlp v1.9.0 h1:l706jCMITVouPOqEnii2fIAuO3IVGBRPV5ICjceRb/A= +go.opentelemetry.io/proto/otlp v1.9.0/go.mod h1:xE+Cx5E/eEHw+ISFkwPLwCZefwVjY+pqKg1qcK03+/4= go.uber.org/atomic v1.7.0 h1:ADUqmZGgLDDfbSL9ZmPxKTybcoEYHgpYfELNoN+7hsw= go.uber.org/atomic v1.7.0/go.mod h1:fEN4uk6kAWBTFdckzkM89CLk9XfWZrxpCo0nPH17wJc= go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto= @@ -217,14 +159,10 @@ go.uber.org/multierr v1.6.0 h1:y6IPFStTAIT5Ytl7/XYmHvzXQ7S3g/IeZW9hyZ5thw4= go.uber.org/multierr v1.6.0/go.mod h1:cdWPpRnG4AhwMwsgIHip0KRBQjJy5kYEpYjJxpXp9iU= go.uber.org/zap v1.17.0 h1:MTjgFu6ZLKvY6Pvaqk97GlxNBuMpV4Hy/3P6tRGlI2U= go.uber.org/zap v1.17.0/go.mod h1:MXVU+bhUf/A7Xi2HNOnopQOrmycQ5Ih87HtOu4q5SSo= -golang.org/x/arch v0.17.0 h1:4O3dfLzd+lQewptAHqjewQZQDyEdejz3VwgeYwkZneU= -golang.org/x/arch v0.17.0/go.mod h1:bdwinDaKcfZUGpH09BB7ZmOfhalA8lQdzl62l8gGWsk= golang.org/x/crypto v0.0.0-20180904163835-0709b304e793/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4= golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= -golang.org/x/crypto v0.38.0 h1:jt+WWG8IZlBnVbomuhg2Mdq0+BBQaHbtqHEFEigjUV8= -golang.org/x/crypto v0.38.0/go.mod h1:MvrbAqul58NNYPKnOra203SB9vpuZW0e+RRZV+Ggqjw= golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= @@ -236,8 +174,8 @@ golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLL golang.org/x/net v0.0.0-20200226121028-0de0cce0169b/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20200625001655-4c5254603344/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA= golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= -golang.org/x/net v0.40.0 h1:79Xs7wF06Gbdcg4kdCCIQArK11Z1hr5POQ6+fIYHNuY= -golang.org/x/net v0.40.0/go.mod h1:y0hY0exeL2Pku80/zKK7tpntoX23cqL3Oa6njdgRtds= +golang.org/x/net v0.49.0 h1:eeHFmOGUTtaaPSGNmjBKpbng9MulQsJURQUAfUwY++o= +golang.org/x/net v0.49.0/go.mod h1:/ysNB2EvaqvesRkuLAyjI1ycPZlQHM3q01F02UY/MV8= golang.org/x/oauth2 v0.0.0-20190226205417-e64efc72b421/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= @@ -257,14 +195,13 @@ golang.org/x/sys v0.0.0-20200625212154-ddb9806d33ae/go.mod h1:h1NjWce9XRLGQEsW7w golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210124154548-22da62e12c0c/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210603081109-ebe580a85c40/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.33.0 h1:q3i8TbbEz+JRD9ywIRlyRAQbM0qF7hu24q3teo2hbuw= -golang.org/x/sys v0.33.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k= +golang.org/x/sys v0.40.0 h1:DBZZqJ2Rkml6QMQsZywtnjnnGvHza6BTfYFWY9kjEWQ= +golang.org/x/sys v0.40.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk= golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= -golang.org/x/text v0.25.0 h1:qVyWApTSYLk/drJRO5mDlNYskwQznZmkpV2c8q9zls4= -golang.org/x/text v0.25.0/go.mod h1:WEdwpYrmk1qmdHvhkSTNPm3app7v4rsT8F2UD6+VHIA= +golang.org/x/text v0.33.0 h1:B3njUFyqtHDUI5jMn1YIr5B0IE2U0qck04r6d4KPAxE= +golang.org/x/text v0.33.0/go.mod h1:LuMebE6+rBincTi9+xWTY8TztLzKHc/9C1uBCG27+q8= golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= golang.org/x/tools v0.0.0-20200619180055-7c47624df98f/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= @@ -273,13 +210,15 @@ golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8T golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +gonum.org/v1/gonum v0.16.0 h1:5+ul4Swaf3ESvrOnidPp4GZbzf0mxVQpDCYUQE7OJfk= +gonum.org/v1/gonum v0.16.0/go.mod h1:fef3am4MQ93R2HHpKnLk4/Tbh/s0+wqD5nfa6Pnwy4E= google.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4= -google.golang.org/genproto/googleapis/api v0.0.0-20250519155744-55703ea1f237 h1:Kog3KlB4xevJlAcbbbzPfRG0+X9fdoGM+UBRKVz6Wr0= -google.golang.org/genproto/googleapis/api v0.0.0-20250519155744-55703ea1f237/go.mod h1:ezi0AVyMKDWy5xAncvjLWH7UcLBB5n7y2fQ8MzjJcto= -google.golang.org/genproto/googleapis/rpc v0.0.0-20250519155744-55703ea1f237 h1:cJfm9zPbe1e873mHJzmQ1nwVEeRDU/T1wXDK2kUSU34= -google.golang.org/genproto/googleapis/rpc v0.0.0-20250519155744-55703ea1f237/go.mod h1:qQ0YXyHHx3XkvlzUtpXDkS29lDSafHMZBAZDc03LQ3A= -google.golang.org/grpc v1.72.1 h1:HR03wO6eyZ7lknl75XlxABNVLLFc2PAb6mHlYh756mA= -google.golang.org/grpc v1.72.1/go.mod h1:wH5Aktxcg25y1I3w7H69nHfXdOG3UiadoBtjh3izSDM= +google.golang.org/genproto/googleapis/api v0.0.0-20260128011058-8636f8732409 h1:merA0rdPeUV3YIIfHHcH4qBkiQAc1nfCKSI7lB4cV2M= +google.golang.org/genproto/googleapis/api v0.0.0-20260128011058-8636f8732409/go.mod h1:fl8J1IvUjCilwZzQowmw2b7HQB2eAuYBabMXzWurF+I= +google.golang.org/genproto/googleapis/rpc v0.0.0-20260128011058-8636f8732409 h1:H86B94AW+VfJWDqFeEbBPhEtHzJwJfTbgE2lZa54ZAQ= +google.golang.org/genproto/googleapis/rpc v0.0.0-20260128011058-8636f8732409/go.mod h1:j9x/tPzZkyxcgEFkiKEEGxfvyumM01BEtsW8xzOahRQ= +google.golang.org/grpc v1.78.0 h1:K1XZG/yGDJnzMdd/uZHAkVqJE+xIDOcmdSFZkBUicNc= +google.golang.org/grpc v1.78.0/go.mod h1:I47qjTo4OKbMkjA/aOOwxDIiPSBofUtQUI5EfpWvW7U= google.golang.org/protobuf v0.0.0-20200109180630-ec00e32a8dfd/go.mod h1:DFci5gLYBciE7Vtevhsrf46CRTquxDuWsQurQQe4oz8= google.golang.org/protobuf v0.0.0-20200221191635-4d8936d0db64/go.mod h1:kwYJMbMJ01Woi6D6+Kah6886xMZcty6N08ah7+eCXa0= google.golang.org/protobuf v0.0.0-20200228230310-ab0ca4ff8a60/go.mod h1:cfTl7dwQJ+fmap5saPgwCLgHXTUD7jkjRqWcaiX5VyM= @@ -287,13 +226,11 @@ google.golang.org/protobuf v1.20.1-0.20200309200217-e05f789c0967/go.mod h1:A+miE google.golang.org/protobuf v1.21.0/go.mod h1:47Nbq4nVaFHyn7ilMalzfO3qCViNmqZ2kzikPIcrTAo= google.golang.org/protobuf v1.23.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU= google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw= -google.golang.org/protobuf v1.36.6 h1:z1NpPI8ku2WgiWnf+t9wTPsn6eP1L7ksHUlkfLvd9xY= -google.golang.org/protobuf v1.36.6/go.mod h1:jduwjTPXsFjZGTmRluh+L6NjiWu7pchiJ2/5YcXBHnY= +google.golang.org/protobuf v1.36.11 h1:fV6ZwhNocDyBLK0dj+fg8ektcVegBBuEolpbTQyBNVE= +google.golang.org/protobuf v1.36.11/go.mod h1:HTf+CrKn2C3g5S8VImy6tdcUvCska2kB7j23XfzDpco= gopkg.in/alecthomas/kingpin.v2 v2.2.6/go.mod h1:FMv+mEhP44yOT+4EoQTLFTRgOQ1FBLkstjWtayDeSgw= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= -gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk= -gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q= gopkg.in/yaml.v2 v2.2.1/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v2 v2.2.4/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= @@ -306,4 +243,3 @@ gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= -nullprogram.com/x/optparse v1.0.0/go.mod h1:KdyPE+Igbe0jQUrVfMqDMeJQIJZEuyV7pjYmp6pbG50= diff --git a/persys-scheduler/internal/agentpb/agent.pb.go b/persys-scheduler/internal/agentpb/agent.pb.go new file mode 100644 index 0000000..154a05f --- /dev/null +++ b/persys-scheduler/internal/agentpb/agent.pb.go @@ -0,0 +1,2163 @@ +// Code generated by protoc-gen-go. DO NOT EDIT. +// versions: +// protoc-gen-go v1.36.11 +// protoc v3.21.12 +// source: agent.proto + +package agentpb + +import ( + protoreflect "google.golang.org/protobuf/reflect/protoreflect" + protoimpl "google.golang.org/protobuf/runtime/protoimpl" + reflect "reflect" + sync "sync" + unsafe "unsafe" +) + +const ( + // Verify that this generated code is sufficiently up-to-date. + _ = protoimpl.EnforceVersion(20 - protoimpl.MinVersion) + // Verify that runtime/protoimpl is sufficiently up-to-date. + _ = protoimpl.EnforceVersion(protoimpl.MaxVersion - 20) +) + +type WorkloadType int32 + +const ( + WorkloadType_WORKLOAD_TYPE_UNSPECIFIED WorkloadType = 0 + WorkloadType_WORKLOAD_TYPE_CONTAINER WorkloadType = 1 + WorkloadType_WORKLOAD_TYPE_COMPOSE WorkloadType = 2 + WorkloadType_WORKLOAD_TYPE_VM WorkloadType = 3 +) + +// Enum value maps for WorkloadType. +var ( + WorkloadType_name = map[int32]string{ + 0: "WORKLOAD_TYPE_UNSPECIFIED", + 1: "WORKLOAD_TYPE_CONTAINER", + 2: "WORKLOAD_TYPE_COMPOSE", + 3: "WORKLOAD_TYPE_VM", + } + WorkloadType_value = map[string]int32{ + "WORKLOAD_TYPE_UNSPECIFIED": 0, + "WORKLOAD_TYPE_CONTAINER": 1, + "WORKLOAD_TYPE_COMPOSE": 2, + "WORKLOAD_TYPE_VM": 3, + } +) + +func (x WorkloadType) Enum() *WorkloadType { + p := new(WorkloadType) + *p = x + return p +} + +func (x WorkloadType) String() string { + return protoimpl.X.EnumStringOf(x.Descriptor(), protoreflect.EnumNumber(x)) +} + +func (WorkloadType) Descriptor() protoreflect.EnumDescriptor { + return file_agent_proto_enumTypes[0].Descriptor() +} + +func (WorkloadType) Type() protoreflect.EnumType { + return &file_agent_proto_enumTypes[0] +} + +func (x WorkloadType) Number() protoreflect.EnumNumber { + return protoreflect.EnumNumber(x) +} + +// Deprecated: Use WorkloadType.Descriptor instead. +func (WorkloadType) EnumDescriptor() ([]byte, []int) { + return file_agent_proto_rawDescGZIP(), []int{0} +} + +type DesiredState int32 + +const ( + DesiredState_DESIRED_STATE_UNSPECIFIED DesiredState = 0 + DesiredState_DESIRED_STATE_RUNNING DesiredState = 1 + DesiredState_DESIRED_STATE_STOPPED DesiredState = 2 +) + +// Enum value maps for DesiredState. +var ( + DesiredState_name = map[int32]string{ + 0: "DESIRED_STATE_UNSPECIFIED", + 1: "DESIRED_STATE_RUNNING", + 2: "DESIRED_STATE_STOPPED", + } + DesiredState_value = map[string]int32{ + "DESIRED_STATE_UNSPECIFIED": 0, + "DESIRED_STATE_RUNNING": 1, + "DESIRED_STATE_STOPPED": 2, + } +) + +func (x DesiredState) Enum() *DesiredState { + p := new(DesiredState) + *p = x + return p +} + +func (x DesiredState) String() string { + return protoimpl.X.EnumStringOf(x.Descriptor(), protoreflect.EnumNumber(x)) +} + +func (DesiredState) Descriptor() protoreflect.EnumDescriptor { + return file_agent_proto_enumTypes[1].Descriptor() +} + +func (DesiredState) Type() protoreflect.EnumType { + return &file_agent_proto_enumTypes[1] +} + +func (x DesiredState) Number() protoreflect.EnumNumber { + return protoreflect.EnumNumber(x) +} + +// Deprecated: Use DesiredState.Descriptor instead. +func (DesiredState) EnumDescriptor() ([]byte, []int) { + return file_agent_proto_rawDescGZIP(), []int{1} +} + +type ActualState int32 + +const ( + ActualState_ACTUAL_STATE_UNSPECIFIED ActualState = 0 + ActualState_ACTUAL_STATE_PENDING ActualState = 1 + ActualState_ACTUAL_STATE_RUNNING ActualState = 2 + ActualState_ACTUAL_STATE_STOPPED ActualState = 3 + ActualState_ACTUAL_STATE_FAILED ActualState = 4 + ActualState_ACTUAL_STATE_UNKNOWN ActualState = 5 +) + +// Enum value maps for ActualState. +var ( + ActualState_name = map[int32]string{ + 0: "ACTUAL_STATE_UNSPECIFIED", + 1: "ACTUAL_STATE_PENDING", + 2: "ACTUAL_STATE_RUNNING", + 3: "ACTUAL_STATE_STOPPED", + 4: "ACTUAL_STATE_FAILED", + 5: "ACTUAL_STATE_UNKNOWN", + } + ActualState_value = map[string]int32{ + "ACTUAL_STATE_UNSPECIFIED": 0, + "ACTUAL_STATE_PENDING": 1, + "ACTUAL_STATE_RUNNING": 2, + "ACTUAL_STATE_STOPPED": 3, + "ACTUAL_STATE_FAILED": 4, + "ACTUAL_STATE_UNKNOWN": 5, + } +) + +func (x ActualState) Enum() *ActualState { + p := new(ActualState) + *p = x + return p +} + +func (x ActualState) String() string { + return protoimpl.X.EnumStringOf(x.Descriptor(), protoreflect.EnumNumber(x)) +} + +func (ActualState) Descriptor() protoreflect.EnumDescriptor { + return file_agent_proto_enumTypes[2].Descriptor() +} + +func (ActualState) Type() protoreflect.EnumType { + return &file_agent_proto_enumTypes[2] +} + +func (x ActualState) Number() protoreflect.EnumNumber { + return protoreflect.EnumNumber(x) +} + +// Deprecated: Use ActualState.Descriptor instead. +func (ActualState) EnumDescriptor() ([]byte, []int) { + return file_agent_proto_rawDescGZIP(), []int{2} +} + +type ApplyWorkloadRequest struct { + state protoimpl.MessageState `protogen:"open.v1"` + Id string `protobuf:"bytes,1,opt,name=id,proto3" json:"id,omitempty"` + Type WorkloadType `protobuf:"varint,2,opt,name=type,proto3,enum=persys.agent.v1.WorkloadType" json:"type,omitempty"` + RevisionId string `protobuf:"bytes,3,opt,name=revision_id,json=revisionId,proto3" json:"revision_id,omitempty"` + DesiredState DesiredState `protobuf:"varint,4,opt,name=desired_state,json=desiredState,proto3,enum=persys.agent.v1.DesiredState" json:"desired_state,omitempty"` + Spec *WorkloadSpec `protobuf:"bytes,5,opt,name=spec,proto3" json:"spec,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *ApplyWorkloadRequest) Reset() { + *x = ApplyWorkloadRequest{} + mi := &file_agent_proto_msgTypes[0] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *ApplyWorkloadRequest) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*ApplyWorkloadRequest) ProtoMessage() {} + +func (x *ApplyWorkloadRequest) ProtoReflect() protoreflect.Message { + mi := &file_agent_proto_msgTypes[0] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use ApplyWorkloadRequest.ProtoReflect.Descriptor instead. +func (*ApplyWorkloadRequest) Descriptor() ([]byte, []int) { + return file_agent_proto_rawDescGZIP(), []int{0} +} + +func (x *ApplyWorkloadRequest) GetId() string { + if x != nil { + return x.Id + } + return "" +} + +func (x *ApplyWorkloadRequest) GetType() WorkloadType { + if x != nil { + return x.Type + } + return WorkloadType_WORKLOAD_TYPE_UNSPECIFIED +} + +func (x *ApplyWorkloadRequest) GetRevisionId() string { + if x != nil { + return x.RevisionId + } + return "" +} + +func (x *ApplyWorkloadRequest) GetDesiredState() DesiredState { + if x != nil { + return x.DesiredState + } + return DesiredState_DESIRED_STATE_UNSPECIFIED +} + +func (x *ApplyWorkloadRequest) GetSpec() *WorkloadSpec { + if x != nil { + return x.Spec + } + return nil +} + +type ApplyWorkloadResponse struct { + state protoimpl.MessageState `protogen:"open.v1"` + Applied bool `protobuf:"varint,1,opt,name=applied,proto3" json:"applied,omitempty"` + Skipped bool `protobuf:"varint,2,opt,name=skipped,proto3" json:"skipped,omitempty"` // true if revision already applied + Message string `protobuf:"bytes,3,opt,name=message,proto3" json:"message,omitempty"` + Status *WorkloadStatus `protobuf:"bytes,4,opt,name=status,proto3" json:"status,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *ApplyWorkloadResponse) Reset() { + *x = ApplyWorkloadResponse{} + mi := &file_agent_proto_msgTypes[1] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *ApplyWorkloadResponse) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*ApplyWorkloadResponse) ProtoMessage() {} + +func (x *ApplyWorkloadResponse) ProtoReflect() protoreflect.Message { + mi := &file_agent_proto_msgTypes[1] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use ApplyWorkloadResponse.ProtoReflect.Descriptor instead. +func (*ApplyWorkloadResponse) Descriptor() ([]byte, []int) { + return file_agent_proto_rawDescGZIP(), []int{1} +} + +func (x *ApplyWorkloadResponse) GetApplied() bool { + if x != nil { + return x.Applied + } + return false +} + +func (x *ApplyWorkloadResponse) GetSkipped() bool { + if x != nil { + return x.Skipped + } + return false +} + +func (x *ApplyWorkloadResponse) GetMessage() string { + if x != nil { + return x.Message + } + return "" +} + +func (x *ApplyWorkloadResponse) GetStatus() *WorkloadStatus { + if x != nil { + return x.Status + } + return nil +} + +type DeleteWorkloadRequest struct { + state protoimpl.MessageState `protogen:"open.v1"` + Id string `protobuf:"bytes,1,opt,name=id,proto3" json:"id,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *DeleteWorkloadRequest) Reset() { + *x = DeleteWorkloadRequest{} + mi := &file_agent_proto_msgTypes[2] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *DeleteWorkloadRequest) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*DeleteWorkloadRequest) ProtoMessage() {} + +func (x *DeleteWorkloadRequest) ProtoReflect() protoreflect.Message { + mi := &file_agent_proto_msgTypes[2] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use DeleteWorkloadRequest.ProtoReflect.Descriptor instead. +func (*DeleteWorkloadRequest) Descriptor() ([]byte, []int) { + return file_agent_proto_rawDescGZIP(), []int{2} +} + +func (x *DeleteWorkloadRequest) GetId() string { + if x != nil { + return x.Id + } + return "" +} + +type DeleteWorkloadResponse struct { + state protoimpl.MessageState `protogen:"open.v1"` + Success bool `protobuf:"varint,1,opt,name=success,proto3" json:"success,omitempty"` + Message string `protobuf:"bytes,2,opt,name=message,proto3" json:"message,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *DeleteWorkloadResponse) Reset() { + *x = DeleteWorkloadResponse{} + mi := &file_agent_proto_msgTypes[3] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *DeleteWorkloadResponse) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*DeleteWorkloadResponse) ProtoMessage() {} + +func (x *DeleteWorkloadResponse) ProtoReflect() protoreflect.Message { + mi := &file_agent_proto_msgTypes[3] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use DeleteWorkloadResponse.ProtoReflect.Descriptor instead. +func (*DeleteWorkloadResponse) Descriptor() ([]byte, []int) { + return file_agent_proto_rawDescGZIP(), []int{3} +} + +func (x *DeleteWorkloadResponse) GetSuccess() bool { + if x != nil { + return x.Success + } + return false +} + +func (x *DeleteWorkloadResponse) GetMessage() string { + if x != nil { + return x.Message + } + return "" +} + +type GetWorkloadStatusRequest struct { + state protoimpl.MessageState `protogen:"open.v1"` + Id string `protobuf:"bytes,1,opt,name=id,proto3" json:"id,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *GetWorkloadStatusRequest) Reset() { + *x = GetWorkloadStatusRequest{} + mi := &file_agent_proto_msgTypes[4] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *GetWorkloadStatusRequest) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*GetWorkloadStatusRequest) ProtoMessage() {} + +func (x *GetWorkloadStatusRequest) ProtoReflect() protoreflect.Message { + mi := &file_agent_proto_msgTypes[4] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use GetWorkloadStatusRequest.ProtoReflect.Descriptor instead. +func (*GetWorkloadStatusRequest) Descriptor() ([]byte, []int) { + return file_agent_proto_rawDescGZIP(), []int{4} +} + +func (x *GetWorkloadStatusRequest) GetId() string { + if x != nil { + return x.Id + } + return "" +} + +type GetWorkloadStatusResponse struct { + state protoimpl.MessageState `protogen:"open.v1"` + Status *WorkloadStatus `protobuf:"bytes,1,opt,name=status,proto3" json:"status,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *GetWorkloadStatusResponse) Reset() { + *x = GetWorkloadStatusResponse{} + mi := &file_agent_proto_msgTypes[5] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *GetWorkloadStatusResponse) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*GetWorkloadStatusResponse) ProtoMessage() {} + +func (x *GetWorkloadStatusResponse) ProtoReflect() protoreflect.Message { + mi := &file_agent_proto_msgTypes[5] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use GetWorkloadStatusResponse.ProtoReflect.Descriptor instead. +func (*GetWorkloadStatusResponse) Descriptor() ([]byte, []int) { + return file_agent_proto_rawDescGZIP(), []int{5} +} + +func (x *GetWorkloadStatusResponse) GetStatus() *WorkloadStatus { + if x != nil { + return x.Status + } + return nil +} + +type ListWorkloadsRequest struct { + state protoimpl.MessageState `protogen:"open.v1"` + Type WorkloadType `protobuf:"varint,1,opt,name=type,proto3,enum=persys.agent.v1.WorkloadType" json:"type,omitempty"` // optional filter + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *ListWorkloadsRequest) Reset() { + *x = ListWorkloadsRequest{} + mi := &file_agent_proto_msgTypes[6] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *ListWorkloadsRequest) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*ListWorkloadsRequest) ProtoMessage() {} + +func (x *ListWorkloadsRequest) ProtoReflect() protoreflect.Message { + mi := &file_agent_proto_msgTypes[6] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use ListWorkloadsRequest.ProtoReflect.Descriptor instead. +func (*ListWorkloadsRequest) Descriptor() ([]byte, []int) { + return file_agent_proto_rawDescGZIP(), []int{6} +} + +func (x *ListWorkloadsRequest) GetType() WorkloadType { + if x != nil { + return x.Type + } + return WorkloadType_WORKLOAD_TYPE_UNSPECIFIED +} + +type ListWorkloadsResponse struct { + state protoimpl.MessageState `protogen:"open.v1"` + Workloads []*WorkloadStatus `protobuf:"bytes,1,rep,name=workloads,proto3" json:"workloads,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *ListWorkloadsResponse) Reset() { + *x = ListWorkloadsResponse{} + mi := &file_agent_proto_msgTypes[7] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *ListWorkloadsResponse) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*ListWorkloadsResponse) ProtoMessage() {} + +func (x *ListWorkloadsResponse) ProtoReflect() protoreflect.Message { + mi := &file_agent_proto_msgTypes[7] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use ListWorkloadsResponse.ProtoReflect.Descriptor instead. +func (*ListWorkloadsResponse) Descriptor() ([]byte, []int) { + return file_agent_proto_rawDescGZIP(), []int{7} +} + +func (x *ListWorkloadsResponse) GetWorkloads() []*WorkloadStatus { + if x != nil { + return x.Workloads + } + return nil +} + +type HealthCheckRequest struct { + state protoimpl.MessageState `protogen:"open.v1"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *HealthCheckRequest) Reset() { + *x = HealthCheckRequest{} + mi := &file_agent_proto_msgTypes[8] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *HealthCheckRequest) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*HealthCheckRequest) ProtoMessage() {} + +func (x *HealthCheckRequest) ProtoReflect() protoreflect.Message { + mi := &file_agent_proto_msgTypes[8] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use HealthCheckRequest.ProtoReflect.Descriptor instead. +func (*HealthCheckRequest) Descriptor() ([]byte, []int) { + return file_agent_proto_rawDescGZIP(), []int{8} +} + +type HealthCheckResponse struct { + state protoimpl.MessageState `protogen:"open.v1"` + Healthy bool `protobuf:"varint,1,opt,name=healthy,proto3" json:"healthy,omitempty"` + Version string `protobuf:"bytes,2,opt,name=version,proto3" json:"version,omitempty"` + RuntimeStatus map[string]string `protobuf:"bytes,3,rep,name=runtime_status,json=runtimeStatus,proto3" json:"runtime_status,omitempty" protobuf_key:"bytes,1,opt,name=key" protobuf_val:"bytes,2,opt,name=value"` + MemoryUtilization float64 `protobuf:"fixed64,4,opt,name=memory_utilization,json=memoryUtilization,proto3" json:"memory_utilization,omitempty"` // Memory utilization percentage (0-100) + CpuUtilization float64 `protobuf:"fixed64,5,opt,name=cpu_utilization,json=cpuUtilization,proto3" json:"cpu_utilization,omitempty"` // CPU utilization percentage (0-100) + DiskUtilization float64 `protobuf:"fixed64,6,opt,name=disk_utilization,json=diskUtilization,proto3" json:"disk_utilization,omitempty"` // Disk utilization percentage for root mount (0-100) + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *HealthCheckResponse) Reset() { + *x = HealthCheckResponse{} + mi := &file_agent_proto_msgTypes[9] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *HealthCheckResponse) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*HealthCheckResponse) ProtoMessage() {} + +func (x *HealthCheckResponse) ProtoReflect() protoreflect.Message { + mi := &file_agent_proto_msgTypes[9] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use HealthCheckResponse.ProtoReflect.Descriptor instead. +func (*HealthCheckResponse) Descriptor() ([]byte, []int) { + return file_agent_proto_rawDescGZIP(), []int{9} +} + +func (x *HealthCheckResponse) GetHealthy() bool { + if x != nil { + return x.Healthy + } + return false +} + +func (x *HealthCheckResponse) GetVersion() string { + if x != nil { + return x.Version + } + return "" +} + +func (x *HealthCheckResponse) GetRuntimeStatus() map[string]string { + if x != nil { + return x.RuntimeStatus + } + return nil +} + +func (x *HealthCheckResponse) GetMemoryUtilization() float64 { + if x != nil { + return x.MemoryUtilization + } + return 0 +} + +func (x *HealthCheckResponse) GetCpuUtilization() float64 { + if x != nil { + return x.CpuUtilization + } + return 0 +} + +func (x *HealthCheckResponse) GetDiskUtilization() float64 { + if x != nil { + return x.DiskUtilization + } + return 0 +} + +type ListActionsRequest struct { + state protoimpl.MessageState `protogen:"open.v1"` + WorkloadId string `protobuf:"bytes,1,opt,name=workload_id,json=workloadId,proto3" json:"workload_id,omitempty"` // optional filter + ActionType string `protobuf:"bytes,2,opt,name=action_type,json=actionType,proto3" json:"action_type,omitempty"` // optional filter: apply_workload, delete_workload, ... + Status string `protobuf:"bytes,3,opt,name=status,proto3" json:"status,omitempty"` // optional filter: pending, running, completed, failed + Limit int32 `protobuf:"varint,4,opt,name=limit,proto3" json:"limit,omitempty"` // optional max number of actions (0 = all) + NewestFirst bool `protobuf:"varint,5,opt,name=newest_first,json=newestFirst,proto3" json:"newest_first,omitempty"` // if true sorts by created_at descending + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *ListActionsRequest) Reset() { + *x = ListActionsRequest{} + mi := &file_agent_proto_msgTypes[10] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *ListActionsRequest) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*ListActionsRequest) ProtoMessage() {} + +func (x *ListActionsRequest) ProtoReflect() protoreflect.Message { + mi := &file_agent_proto_msgTypes[10] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use ListActionsRequest.ProtoReflect.Descriptor instead. +func (*ListActionsRequest) Descriptor() ([]byte, []int) { + return file_agent_proto_rawDescGZIP(), []int{10} +} + +func (x *ListActionsRequest) GetWorkloadId() string { + if x != nil { + return x.WorkloadId + } + return "" +} + +func (x *ListActionsRequest) GetActionType() string { + if x != nil { + return x.ActionType + } + return "" +} + +func (x *ListActionsRequest) GetStatus() string { + if x != nil { + return x.Status + } + return "" +} + +func (x *ListActionsRequest) GetLimit() int32 { + if x != nil { + return x.Limit + } + return 0 +} + +func (x *ListActionsRequest) GetNewestFirst() bool { + if x != nil { + return x.NewestFirst + } + return false +} + +type AgentAction struct { + state protoimpl.MessageState `protogen:"open.v1"` + TaskId string `protobuf:"bytes,1,opt,name=task_id,json=taskId,proto3" json:"task_id,omitempty"` + WorkloadId string `protobuf:"bytes,2,opt,name=workload_id,json=workloadId,proto3" json:"workload_id,omitempty"` + ActionType string `protobuf:"bytes,3,opt,name=action_type,json=actionType,proto3" json:"action_type,omitempty"` + Status string `protobuf:"bytes,4,opt,name=status,proto3" json:"status,omitempty"` + Error string `protobuf:"bytes,5,opt,name=error,proto3" json:"error,omitempty"` + CreatedAt int64 `protobuf:"varint,6,opt,name=created_at,json=createdAt,proto3" json:"created_at,omitempty"` + StartedAt int64 `protobuf:"varint,7,opt,name=started_at,json=startedAt,proto3" json:"started_at,omitempty"` + EndedAt int64 `protobuf:"varint,8,opt,name=ended_at,json=endedAt,proto3" json:"ended_at,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *AgentAction) Reset() { + *x = AgentAction{} + mi := &file_agent_proto_msgTypes[11] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *AgentAction) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*AgentAction) ProtoMessage() {} + +func (x *AgentAction) ProtoReflect() protoreflect.Message { + mi := &file_agent_proto_msgTypes[11] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use AgentAction.ProtoReflect.Descriptor instead. +func (*AgentAction) Descriptor() ([]byte, []int) { + return file_agent_proto_rawDescGZIP(), []int{11} +} + +func (x *AgentAction) GetTaskId() string { + if x != nil { + return x.TaskId + } + return "" +} + +func (x *AgentAction) GetWorkloadId() string { + if x != nil { + return x.WorkloadId + } + return "" +} + +func (x *AgentAction) GetActionType() string { + if x != nil { + return x.ActionType + } + return "" +} + +func (x *AgentAction) GetStatus() string { + if x != nil { + return x.Status + } + return "" +} + +func (x *AgentAction) GetError() string { + if x != nil { + return x.Error + } + return "" +} + +func (x *AgentAction) GetCreatedAt() int64 { + if x != nil { + return x.CreatedAt + } + return 0 +} + +func (x *AgentAction) GetStartedAt() int64 { + if x != nil { + return x.StartedAt + } + return 0 +} + +func (x *AgentAction) GetEndedAt() int64 { + if x != nil { + return x.EndedAt + } + return 0 +} + +type ListActionsResponse struct { + state protoimpl.MessageState `protogen:"open.v1"` + Actions []*AgentAction `protobuf:"bytes,1,rep,name=actions,proto3" json:"actions,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *ListActionsResponse) Reset() { + *x = ListActionsResponse{} + mi := &file_agent_proto_msgTypes[12] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *ListActionsResponse) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*ListActionsResponse) ProtoMessage() {} + +func (x *ListActionsResponse) ProtoReflect() protoreflect.Message { + mi := &file_agent_proto_msgTypes[12] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use ListActionsResponse.ProtoReflect.Descriptor instead. +func (*ListActionsResponse) Descriptor() ([]byte, []int) { + return file_agent_proto_rawDescGZIP(), []int{12} +} + +func (x *ListActionsResponse) GetActions() []*AgentAction { + if x != nil { + return x.Actions + } + return nil +} + +type WorkloadSpec struct { + state protoimpl.MessageState `protogen:"open.v1"` + // Types that are valid to be assigned to Spec: + // + // *WorkloadSpec_Container + // *WorkloadSpec_Compose + // *WorkloadSpec_Vm + Spec isWorkloadSpec_Spec `protobuf_oneof:"spec"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *WorkloadSpec) Reset() { + *x = WorkloadSpec{} + mi := &file_agent_proto_msgTypes[13] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *WorkloadSpec) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*WorkloadSpec) ProtoMessage() {} + +func (x *WorkloadSpec) ProtoReflect() protoreflect.Message { + mi := &file_agent_proto_msgTypes[13] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use WorkloadSpec.ProtoReflect.Descriptor instead. +func (*WorkloadSpec) Descriptor() ([]byte, []int) { + return file_agent_proto_rawDescGZIP(), []int{13} +} + +func (x *WorkloadSpec) GetSpec() isWorkloadSpec_Spec { + if x != nil { + return x.Spec + } + return nil +} + +func (x *WorkloadSpec) GetContainer() *ContainerSpec { + if x != nil { + if x, ok := x.Spec.(*WorkloadSpec_Container); ok { + return x.Container + } + } + return nil +} + +func (x *WorkloadSpec) GetCompose() *ComposeSpec { + if x != nil { + if x, ok := x.Spec.(*WorkloadSpec_Compose); ok { + return x.Compose + } + } + return nil +} + +func (x *WorkloadSpec) GetVm() *VMSpec { + if x != nil { + if x, ok := x.Spec.(*WorkloadSpec_Vm); ok { + return x.Vm + } + } + return nil +} + +type isWorkloadSpec_Spec interface { + isWorkloadSpec_Spec() +} + +type WorkloadSpec_Container struct { + Container *ContainerSpec `protobuf:"bytes,1,opt,name=container,proto3,oneof"` +} + +type WorkloadSpec_Compose struct { + Compose *ComposeSpec `protobuf:"bytes,2,opt,name=compose,proto3,oneof"` +} + +type WorkloadSpec_Vm struct { + Vm *VMSpec `protobuf:"bytes,3,opt,name=vm,proto3,oneof"` +} + +func (*WorkloadSpec_Container) isWorkloadSpec_Spec() {} + +func (*WorkloadSpec_Compose) isWorkloadSpec_Spec() {} + +func (*WorkloadSpec_Vm) isWorkloadSpec_Spec() {} + +type ContainerSpec struct { + state protoimpl.MessageState `protogen:"open.v1"` + Image string `protobuf:"bytes,1,opt,name=image,proto3" json:"image,omitempty"` + Command []string `protobuf:"bytes,2,rep,name=command,proto3" json:"command,omitempty"` + Args []string `protobuf:"bytes,3,rep,name=args,proto3" json:"args,omitempty"` + Env map[string]string `protobuf:"bytes,4,rep,name=env,proto3" json:"env,omitempty" protobuf_key:"bytes,1,opt,name=key" protobuf_val:"bytes,2,opt,name=value"` + Volumes []*VolumeMount `protobuf:"bytes,5,rep,name=volumes,proto3" json:"volumes,omitempty"` + Ports []*PortMapping `protobuf:"bytes,6,rep,name=ports,proto3" json:"ports,omitempty"` + Resources *ResourceLimits `protobuf:"bytes,7,opt,name=resources,proto3" json:"resources,omitempty"` + RestartPolicy *RestartPolicy `protobuf:"bytes,8,opt,name=restart_policy,json=restartPolicy,proto3" json:"restart_policy,omitempty"` + Labels map[string]string `protobuf:"bytes,9,rep,name=labels,proto3" json:"labels,omitempty" protobuf_key:"bytes,1,opt,name=key" protobuf_val:"bytes,2,opt,name=value"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *ContainerSpec) Reset() { + *x = ContainerSpec{} + mi := &file_agent_proto_msgTypes[14] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *ContainerSpec) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*ContainerSpec) ProtoMessage() {} + +func (x *ContainerSpec) ProtoReflect() protoreflect.Message { + mi := &file_agent_proto_msgTypes[14] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use ContainerSpec.ProtoReflect.Descriptor instead. +func (*ContainerSpec) Descriptor() ([]byte, []int) { + return file_agent_proto_rawDescGZIP(), []int{14} +} + +func (x *ContainerSpec) GetImage() string { + if x != nil { + return x.Image + } + return "" +} + +func (x *ContainerSpec) GetCommand() []string { + if x != nil { + return x.Command + } + return nil +} + +func (x *ContainerSpec) GetArgs() []string { + if x != nil { + return x.Args + } + return nil +} + +func (x *ContainerSpec) GetEnv() map[string]string { + if x != nil { + return x.Env + } + return nil +} + +func (x *ContainerSpec) GetVolumes() []*VolumeMount { + if x != nil { + return x.Volumes + } + return nil +} + +func (x *ContainerSpec) GetPorts() []*PortMapping { + if x != nil { + return x.Ports + } + return nil +} + +func (x *ContainerSpec) GetResources() *ResourceLimits { + if x != nil { + return x.Resources + } + return nil +} + +func (x *ContainerSpec) GetRestartPolicy() *RestartPolicy { + if x != nil { + return x.RestartPolicy + } + return nil +} + +func (x *ContainerSpec) GetLabels() map[string]string { + if x != nil { + return x.Labels + } + return nil +} + +type ComposeSpec struct { + state protoimpl.MessageState `protogen:"open.v1"` + ProjectName string `protobuf:"bytes,1,opt,name=project_name,json=projectName,proto3" json:"project_name,omitempty"` + ComposeYaml string `protobuf:"bytes,2,opt,name=compose_yaml,json=composeYaml,proto3" json:"compose_yaml,omitempty"` // base64 encoded docker-compose.yml + Env map[string]string `protobuf:"bytes,3,rep,name=env,proto3" json:"env,omitempty" protobuf_key:"bytes,1,opt,name=key" protobuf_val:"bytes,2,opt,name=value"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *ComposeSpec) Reset() { + *x = ComposeSpec{} + mi := &file_agent_proto_msgTypes[15] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *ComposeSpec) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*ComposeSpec) ProtoMessage() {} + +func (x *ComposeSpec) ProtoReflect() protoreflect.Message { + mi := &file_agent_proto_msgTypes[15] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use ComposeSpec.ProtoReflect.Descriptor instead. +func (*ComposeSpec) Descriptor() ([]byte, []int) { + return file_agent_proto_rawDescGZIP(), []int{15} +} + +func (x *ComposeSpec) GetProjectName() string { + if x != nil { + return x.ProjectName + } + return "" +} + +func (x *ComposeSpec) GetComposeYaml() string { + if x != nil { + return x.ComposeYaml + } + return "" +} + +func (x *ComposeSpec) GetEnv() map[string]string { + if x != nil { + return x.Env + } + return nil +} + +type VMSpec struct { + state protoimpl.MessageState `protogen:"open.v1"` + Name string `protobuf:"bytes,1,opt,name=name,proto3" json:"name,omitempty"` + Vcpus int32 `protobuf:"varint,2,opt,name=vcpus,proto3" json:"vcpus,omitempty"` + MemoryMb int64 `protobuf:"varint,3,opt,name=memory_mb,json=memoryMb,proto3" json:"memory_mb,omitempty"` + Disks []*DiskConfig `protobuf:"bytes,4,rep,name=disks,proto3" json:"disks,omitempty"` + Networks []*NetworkConfig `protobuf:"bytes,5,rep,name=networks,proto3" json:"networks,omitempty"` + CloudInit string `protobuf:"bytes,6,opt,name=cloud_init,json=cloudInit,proto3" json:"cloud_init,omitempty"` // optional cloud-init user-data (YAML content) + Metadata map[string]string `protobuf:"bytes,7,rep,name=metadata,proto3" json:"metadata,omitempty" protobuf_key:"bytes,1,opt,name=key" protobuf_val:"bytes,2,opt,name=value"` + CloudInitConfig *CloudInitConfig `protobuf:"bytes,8,opt,name=cloud_init_config,json=cloudInitConfig,proto3" json:"cloud_init_config,omitempty"` // advanced cloud-init settings + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *VMSpec) Reset() { + *x = VMSpec{} + mi := &file_agent_proto_msgTypes[16] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *VMSpec) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*VMSpec) ProtoMessage() {} + +func (x *VMSpec) ProtoReflect() protoreflect.Message { + mi := &file_agent_proto_msgTypes[16] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use VMSpec.ProtoReflect.Descriptor instead. +func (*VMSpec) Descriptor() ([]byte, []int) { + return file_agent_proto_rawDescGZIP(), []int{16} +} + +func (x *VMSpec) GetName() string { + if x != nil { + return x.Name + } + return "" +} + +func (x *VMSpec) GetVcpus() int32 { + if x != nil { + return x.Vcpus + } + return 0 +} + +func (x *VMSpec) GetMemoryMb() int64 { + if x != nil { + return x.MemoryMb + } + return 0 +} + +func (x *VMSpec) GetDisks() []*DiskConfig { + if x != nil { + return x.Disks + } + return nil +} + +func (x *VMSpec) GetNetworks() []*NetworkConfig { + if x != nil { + return x.Networks + } + return nil +} + +func (x *VMSpec) GetCloudInit() string { + if x != nil { + return x.CloudInit + } + return "" +} + +func (x *VMSpec) GetMetadata() map[string]string { + if x != nil { + return x.Metadata + } + return nil +} + +func (x *VMSpec) GetCloudInitConfig() *CloudInitConfig { + if x != nil { + return x.CloudInitConfig + } + return nil +} + +type CloudInitConfig struct { + state protoimpl.MessageState `protogen:"open.v1"` + UserData string `protobuf:"bytes,1,opt,name=user_data,json=userData,proto3" json:"user_data,omitempty"` // cloud-init user-data script + MetaData string `protobuf:"bytes,2,opt,name=meta_data,json=metaData,proto3" json:"meta_data,omitempty"` // cloud-init meta-data (JSON) + NetworkConfig string `protobuf:"bytes,3,opt,name=network_config,json=networkConfig,proto3" json:"network_config,omitempty"` // cloud-init network config (YAML) + VendorData string `protobuf:"bytes,4,opt,name=vendor_data,json=vendorData,proto3" json:"vendor_data,omitempty"` // cloud-init vendor-data + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *CloudInitConfig) Reset() { + *x = CloudInitConfig{} + mi := &file_agent_proto_msgTypes[17] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *CloudInitConfig) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*CloudInitConfig) ProtoMessage() {} + +func (x *CloudInitConfig) ProtoReflect() protoreflect.Message { + mi := &file_agent_proto_msgTypes[17] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use CloudInitConfig.ProtoReflect.Descriptor instead. +func (*CloudInitConfig) Descriptor() ([]byte, []int) { + return file_agent_proto_rawDescGZIP(), []int{17} +} + +func (x *CloudInitConfig) GetUserData() string { + if x != nil { + return x.UserData + } + return "" +} + +func (x *CloudInitConfig) GetMetaData() string { + if x != nil { + return x.MetaData + } + return "" +} + +func (x *CloudInitConfig) GetNetworkConfig() string { + if x != nil { + return x.NetworkConfig + } + return "" +} + +func (x *CloudInitConfig) GetVendorData() string { + if x != nil { + return x.VendorData + } + return "" +} + +type VolumeMount struct { + state protoimpl.MessageState `protogen:"open.v1"` + HostPath string `protobuf:"bytes,1,opt,name=host_path,json=hostPath,proto3" json:"host_path,omitempty"` + ContainerPath string `protobuf:"bytes,2,opt,name=container_path,json=containerPath,proto3" json:"container_path,omitempty"` + ReadOnly bool `protobuf:"varint,3,opt,name=read_only,json=readOnly,proto3" json:"read_only,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *VolumeMount) Reset() { + *x = VolumeMount{} + mi := &file_agent_proto_msgTypes[18] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *VolumeMount) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*VolumeMount) ProtoMessage() {} + +func (x *VolumeMount) ProtoReflect() protoreflect.Message { + mi := &file_agent_proto_msgTypes[18] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use VolumeMount.ProtoReflect.Descriptor instead. +func (*VolumeMount) Descriptor() ([]byte, []int) { + return file_agent_proto_rawDescGZIP(), []int{18} +} + +func (x *VolumeMount) GetHostPath() string { + if x != nil { + return x.HostPath + } + return "" +} + +func (x *VolumeMount) GetContainerPath() string { + if x != nil { + return x.ContainerPath + } + return "" +} + +func (x *VolumeMount) GetReadOnly() bool { + if x != nil { + return x.ReadOnly + } + return false +} + +type PortMapping struct { + state protoimpl.MessageState `protogen:"open.v1"` + HostPort int32 `protobuf:"varint,1,opt,name=host_port,json=hostPort,proto3" json:"host_port,omitempty"` + ContainerPort int32 `protobuf:"varint,2,opt,name=container_port,json=containerPort,proto3" json:"container_port,omitempty"` + Protocol string `protobuf:"bytes,3,opt,name=protocol,proto3" json:"protocol,omitempty"` // tcp or udp + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *PortMapping) Reset() { + *x = PortMapping{} + mi := &file_agent_proto_msgTypes[19] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *PortMapping) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*PortMapping) ProtoMessage() {} + +func (x *PortMapping) ProtoReflect() protoreflect.Message { + mi := &file_agent_proto_msgTypes[19] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use PortMapping.ProtoReflect.Descriptor instead. +func (*PortMapping) Descriptor() ([]byte, []int) { + return file_agent_proto_rawDescGZIP(), []int{19} +} + +func (x *PortMapping) GetHostPort() int32 { + if x != nil { + return x.HostPort + } + return 0 +} + +func (x *PortMapping) GetContainerPort() int32 { + if x != nil { + return x.ContainerPort + } + return 0 +} + +func (x *PortMapping) GetProtocol() string { + if x != nil { + return x.Protocol + } + return "" +} + +type ResourceLimits struct { + state protoimpl.MessageState `protogen:"open.v1"` + CpuShares int64 `protobuf:"varint,1,opt,name=cpu_shares,json=cpuShares,proto3" json:"cpu_shares,omitempty"` + MemoryBytes int64 `protobuf:"varint,2,opt,name=memory_bytes,json=memoryBytes,proto3" json:"memory_bytes,omitempty"` + MemorySwapBytes int64 `protobuf:"varint,3,opt,name=memory_swap_bytes,json=memorySwapBytes,proto3" json:"memory_swap_bytes,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *ResourceLimits) Reset() { + *x = ResourceLimits{} + mi := &file_agent_proto_msgTypes[20] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *ResourceLimits) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*ResourceLimits) ProtoMessage() {} + +func (x *ResourceLimits) ProtoReflect() protoreflect.Message { + mi := &file_agent_proto_msgTypes[20] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use ResourceLimits.ProtoReflect.Descriptor instead. +func (*ResourceLimits) Descriptor() ([]byte, []int) { + return file_agent_proto_rawDescGZIP(), []int{20} +} + +func (x *ResourceLimits) GetCpuShares() int64 { + if x != nil { + return x.CpuShares + } + return 0 +} + +func (x *ResourceLimits) GetMemoryBytes() int64 { + if x != nil { + return x.MemoryBytes + } + return 0 +} + +func (x *ResourceLimits) GetMemorySwapBytes() int64 { + if x != nil { + return x.MemorySwapBytes + } + return 0 +} + +type RestartPolicy struct { + state protoimpl.MessageState `protogen:"open.v1"` + Policy string `protobuf:"bytes,1,opt,name=policy,proto3" json:"policy,omitempty"` // no, always, on-failure, unless-stopped + MaxRetryCount int32 `protobuf:"varint,2,opt,name=max_retry_count,json=maxRetryCount,proto3" json:"max_retry_count,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *RestartPolicy) Reset() { + *x = RestartPolicy{} + mi := &file_agent_proto_msgTypes[21] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *RestartPolicy) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*RestartPolicy) ProtoMessage() {} + +func (x *RestartPolicy) ProtoReflect() protoreflect.Message { + mi := &file_agent_proto_msgTypes[21] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use RestartPolicy.ProtoReflect.Descriptor instead. +func (*RestartPolicy) Descriptor() ([]byte, []int) { + return file_agent_proto_rawDescGZIP(), []int{21} +} + +func (x *RestartPolicy) GetPolicy() string { + if x != nil { + return x.Policy + } + return "" +} + +func (x *RestartPolicy) GetMaxRetryCount() int32 { + if x != nil { + return x.MaxRetryCount + } + return 0 +} + +type DiskConfig struct { + state protoimpl.MessageState `protogen:"open.v1"` + Path string `protobuf:"bytes,1,opt,name=path,proto3" json:"path,omitempty"` // path to disk image or ISO + Device string `protobuf:"bytes,2,opt,name=device,proto3" json:"device,omitempty"` // vda, vdb, etc. + Format string `protobuf:"bytes,3,opt,name=format,proto3" json:"format,omitempty"` // qcow2, raw, iso + SizeGb int64 `protobuf:"varint,4,opt,name=size_gb,json=sizeGb,proto3" json:"size_gb,omitempty"` + Type string `protobuf:"bytes,5,opt,name=type,proto3" json:"type,omitempty"` // disk or cdrom (for ISO) + Boot bool `protobuf:"varint,6,opt,name=boot,proto3" json:"boot,omitempty"` // true if this is the boot disk/ISO + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *DiskConfig) Reset() { + *x = DiskConfig{} + mi := &file_agent_proto_msgTypes[22] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *DiskConfig) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*DiskConfig) ProtoMessage() {} + +func (x *DiskConfig) ProtoReflect() protoreflect.Message { + mi := &file_agent_proto_msgTypes[22] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use DiskConfig.ProtoReflect.Descriptor instead. +func (*DiskConfig) Descriptor() ([]byte, []int) { + return file_agent_proto_rawDescGZIP(), []int{22} +} + +func (x *DiskConfig) GetPath() string { + if x != nil { + return x.Path + } + return "" +} + +func (x *DiskConfig) GetDevice() string { + if x != nil { + return x.Device + } + return "" +} + +func (x *DiskConfig) GetFormat() string { + if x != nil { + return x.Format + } + return "" +} + +func (x *DiskConfig) GetSizeGb() int64 { + if x != nil { + return x.SizeGb + } + return 0 +} + +func (x *DiskConfig) GetType() string { + if x != nil { + return x.Type + } + return "" +} + +func (x *DiskConfig) GetBoot() bool { + if x != nil { + return x.Boot + } + return false +} + +type NetworkConfig struct { + state protoimpl.MessageState `protogen:"open.v1"` + Network string `protobuf:"bytes,1,opt,name=network,proto3" json:"network,omitempty"` // network name or bridge + MacAddress string `protobuf:"bytes,2,opt,name=mac_address,json=macAddress,proto3" json:"mac_address,omitempty"` + IpAddress string `protobuf:"bytes,3,opt,name=ip_address,json=ipAddress,proto3" json:"ip_address,omitempty"` // optional static IP + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *NetworkConfig) Reset() { + *x = NetworkConfig{} + mi := &file_agent_proto_msgTypes[23] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *NetworkConfig) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*NetworkConfig) ProtoMessage() {} + +func (x *NetworkConfig) ProtoReflect() protoreflect.Message { + mi := &file_agent_proto_msgTypes[23] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use NetworkConfig.ProtoReflect.Descriptor instead. +func (*NetworkConfig) Descriptor() ([]byte, []int) { + return file_agent_proto_rawDescGZIP(), []int{23} +} + +func (x *NetworkConfig) GetNetwork() string { + if x != nil { + return x.Network + } + return "" +} + +func (x *NetworkConfig) GetMacAddress() string { + if x != nil { + return x.MacAddress + } + return "" +} + +func (x *NetworkConfig) GetIpAddress() string { + if x != nil { + return x.IpAddress + } + return "" +} + +type WorkloadStatus struct { + state protoimpl.MessageState `protogen:"open.v1"` + Id string `protobuf:"bytes,1,opt,name=id,proto3" json:"id,omitempty"` + Type WorkloadType `protobuf:"varint,2,opt,name=type,proto3,enum=persys.agent.v1.WorkloadType" json:"type,omitempty"` + RevisionId string `protobuf:"bytes,3,opt,name=revision_id,json=revisionId,proto3" json:"revision_id,omitempty"` + DesiredState DesiredState `protobuf:"varint,4,opt,name=desired_state,json=desiredState,proto3,enum=persys.agent.v1.DesiredState" json:"desired_state,omitempty"` + ActualState ActualState `protobuf:"varint,5,opt,name=actual_state,json=actualState,proto3,enum=persys.agent.v1.ActualState" json:"actual_state,omitempty"` + Message string `protobuf:"bytes,6,opt,name=message,proto3" json:"message,omitempty"` + CreatedAt int64 `protobuf:"varint,7,opt,name=created_at,json=createdAt,proto3" json:"created_at,omitempty"` + UpdatedAt int64 `protobuf:"varint,8,opt,name=updated_at,json=updatedAt,proto3" json:"updated_at,omitempty"` + Metadata map[string]string `protobuf:"bytes,9,rep,name=metadata,proto3" json:"metadata,omitempty" protobuf_key:"bytes,1,opt,name=key" protobuf_val:"bytes,2,opt,name=value"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *WorkloadStatus) Reset() { + *x = WorkloadStatus{} + mi := &file_agent_proto_msgTypes[24] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *WorkloadStatus) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*WorkloadStatus) ProtoMessage() {} + +func (x *WorkloadStatus) ProtoReflect() protoreflect.Message { + mi := &file_agent_proto_msgTypes[24] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use WorkloadStatus.ProtoReflect.Descriptor instead. +func (*WorkloadStatus) Descriptor() ([]byte, []int) { + return file_agent_proto_rawDescGZIP(), []int{24} +} + +func (x *WorkloadStatus) GetId() string { + if x != nil { + return x.Id + } + return "" +} + +func (x *WorkloadStatus) GetType() WorkloadType { + if x != nil { + return x.Type + } + return WorkloadType_WORKLOAD_TYPE_UNSPECIFIED +} + +func (x *WorkloadStatus) GetRevisionId() string { + if x != nil { + return x.RevisionId + } + return "" +} + +func (x *WorkloadStatus) GetDesiredState() DesiredState { + if x != nil { + return x.DesiredState + } + return DesiredState_DESIRED_STATE_UNSPECIFIED +} + +func (x *WorkloadStatus) GetActualState() ActualState { + if x != nil { + return x.ActualState + } + return ActualState_ACTUAL_STATE_UNSPECIFIED +} + +func (x *WorkloadStatus) GetMessage() string { + if x != nil { + return x.Message + } + return "" +} + +func (x *WorkloadStatus) GetCreatedAt() int64 { + if x != nil { + return x.CreatedAt + } + return 0 +} + +func (x *WorkloadStatus) GetUpdatedAt() int64 { + if x != nil { + return x.UpdatedAt + } + return 0 +} + +func (x *WorkloadStatus) GetMetadata() map[string]string { + if x != nil { + return x.Metadata + } + return nil +} + +var File_agent_proto protoreflect.FileDescriptor + +const file_agent_proto_rawDesc = "" + + "\n" + + "\vagent.proto\x12\x0fpersys.agent.v1\"\xf1\x01\n" + + "\x14ApplyWorkloadRequest\x12\x0e\n" + + "\x02id\x18\x01 \x01(\tR\x02id\x121\n" + + "\x04type\x18\x02 \x01(\x0e2\x1d.persys.agent.v1.WorkloadTypeR\x04type\x12\x1f\n" + + "\vrevision_id\x18\x03 \x01(\tR\n" + + "revisionId\x12B\n" + + "\rdesired_state\x18\x04 \x01(\x0e2\x1d.persys.agent.v1.DesiredStateR\fdesiredState\x121\n" + + "\x04spec\x18\x05 \x01(\v2\x1d.persys.agent.v1.WorkloadSpecR\x04spec\"\x9e\x01\n" + + "\x15ApplyWorkloadResponse\x12\x18\n" + + "\aapplied\x18\x01 \x01(\bR\aapplied\x12\x18\n" + + "\askipped\x18\x02 \x01(\bR\askipped\x12\x18\n" + + "\amessage\x18\x03 \x01(\tR\amessage\x127\n" + + "\x06status\x18\x04 \x01(\v2\x1f.persys.agent.v1.WorkloadStatusR\x06status\"'\n" + + "\x15DeleteWorkloadRequest\x12\x0e\n" + + "\x02id\x18\x01 \x01(\tR\x02id\"L\n" + + "\x16DeleteWorkloadResponse\x12\x18\n" + + "\asuccess\x18\x01 \x01(\bR\asuccess\x12\x18\n" + + "\amessage\x18\x02 \x01(\tR\amessage\"*\n" + + "\x18GetWorkloadStatusRequest\x12\x0e\n" + + "\x02id\x18\x01 \x01(\tR\x02id\"T\n" + + "\x19GetWorkloadStatusResponse\x127\n" + + "\x06status\x18\x01 \x01(\v2\x1f.persys.agent.v1.WorkloadStatusR\x06status\"I\n" + + "\x14ListWorkloadsRequest\x121\n" + + "\x04type\x18\x01 \x01(\x0e2\x1d.persys.agent.v1.WorkloadTypeR\x04type\"V\n" + + "\x15ListWorkloadsResponse\x12=\n" + + "\tworkloads\x18\x01 \x03(\v2\x1f.persys.agent.v1.WorkloadStatusR\tworkloads\"\x14\n" + + "\x12HealthCheckRequest\"\xee\x02\n" + + "\x13HealthCheckResponse\x12\x18\n" + + "\ahealthy\x18\x01 \x01(\bR\ahealthy\x12\x18\n" + + "\aversion\x18\x02 \x01(\tR\aversion\x12^\n" + + "\x0eruntime_status\x18\x03 \x03(\v27.persys.agent.v1.HealthCheckResponse.RuntimeStatusEntryR\rruntimeStatus\x12-\n" + + "\x12memory_utilization\x18\x04 \x01(\x01R\x11memoryUtilization\x12'\n" + + "\x0fcpu_utilization\x18\x05 \x01(\x01R\x0ecpuUtilization\x12)\n" + + "\x10disk_utilization\x18\x06 \x01(\x01R\x0fdiskUtilization\x1a@\n" + + "\x12RuntimeStatusEntry\x12\x10\n" + + "\x03key\x18\x01 \x01(\tR\x03key\x12\x14\n" + + "\x05value\x18\x02 \x01(\tR\x05value:\x028\x01\"\xa7\x01\n" + + "\x12ListActionsRequest\x12\x1f\n" + + "\vworkload_id\x18\x01 \x01(\tR\n" + + "workloadId\x12\x1f\n" + + "\vaction_type\x18\x02 \x01(\tR\n" + + "actionType\x12\x16\n" + + "\x06status\x18\x03 \x01(\tR\x06status\x12\x14\n" + + "\x05limit\x18\x04 \x01(\x05R\x05limit\x12!\n" + + "\fnewest_first\x18\x05 \x01(\bR\vnewestFirst\"\xef\x01\n" + + "\vAgentAction\x12\x17\n" + + "\atask_id\x18\x01 \x01(\tR\x06taskId\x12\x1f\n" + + "\vworkload_id\x18\x02 \x01(\tR\n" + + "workloadId\x12\x1f\n" + + "\vaction_type\x18\x03 \x01(\tR\n" + + "actionType\x12\x16\n" + + "\x06status\x18\x04 \x01(\tR\x06status\x12\x14\n" + + "\x05error\x18\x05 \x01(\tR\x05error\x12\x1d\n" + + "\n" + + "created_at\x18\x06 \x01(\x03R\tcreatedAt\x12\x1d\n" + + "\n" + + "started_at\x18\a \x01(\x03R\tstartedAt\x12\x19\n" + + "\bended_at\x18\b \x01(\x03R\aendedAt\"M\n" + + "\x13ListActionsResponse\x126\n" + + "\aactions\x18\x01 \x03(\v2\x1c.persys.agent.v1.AgentActionR\aactions\"\xbb\x01\n" + + "\fWorkloadSpec\x12>\n" + + "\tcontainer\x18\x01 \x01(\v2\x1e.persys.agent.v1.ContainerSpecH\x00R\tcontainer\x128\n" + + "\acompose\x18\x02 \x01(\v2\x1c.persys.agent.v1.ComposeSpecH\x00R\acompose\x12)\n" + + "\x02vm\x18\x03 \x01(\v2\x17.persys.agent.v1.VMSpecH\x00R\x02vmB\x06\n" + + "\x04spec\"\xb7\x04\n" + + "\rContainerSpec\x12\x14\n" + + "\x05image\x18\x01 \x01(\tR\x05image\x12\x18\n" + + "\acommand\x18\x02 \x03(\tR\acommand\x12\x12\n" + + "\x04args\x18\x03 \x03(\tR\x04args\x129\n" + + "\x03env\x18\x04 \x03(\v2'.persys.agent.v1.ContainerSpec.EnvEntryR\x03env\x126\n" + + "\avolumes\x18\x05 \x03(\v2\x1c.persys.agent.v1.VolumeMountR\avolumes\x122\n" + + "\x05ports\x18\x06 \x03(\v2\x1c.persys.agent.v1.PortMappingR\x05ports\x12=\n" + + "\tresources\x18\a \x01(\v2\x1f.persys.agent.v1.ResourceLimitsR\tresources\x12E\n" + + "\x0erestart_policy\x18\b \x01(\v2\x1e.persys.agent.v1.RestartPolicyR\rrestartPolicy\x12B\n" + + "\x06labels\x18\t \x03(\v2*.persys.agent.v1.ContainerSpec.LabelsEntryR\x06labels\x1a6\n" + + "\bEnvEntry\x12\x10\n" + + "\x03key\x18\x01 \x01(\tR\x03key\x12\x14\n" + + "\x05value\x18\x02 \x01(\tR\x05value:\x028\x01\x1a9\n" + + "\vLabelsEntry\x12\x10\n" + + "\x03key\x18\x01 \x01(\tR\x03key\x12\x14\n" + + "\x05value\x18\x02 \x01(\tR\x05value:\x028\x01\"\xc4\x01\n" + + "\vComposeSpec\x12!\n" + + "\fproject_name\x18\x01 \x01(\tR\vprojectName\x12!\n" + + "\fcompose_yaml\x18\x02 \x01(\tR\vcomposeYaml\x127\n" + + "\x03env\x18\x03 \x03(\v2%.persys.agent.v1.ComposeSpec.EnvEntryR\x03env\x1a6\n" + + "\bEnvEntry\x12\x10\n" + + "\x03key\x18\x01 \x01(\tR\x03key\x12\x14\n" + + "\x05value\x18\x02 \x01(\tR\x05value:\x028\x01\"\xab\x03\n" + + "\x06VMSpec\x12\x12\n" + + "\x04name\x18\x01 \x01(\tR\x04name\x12\x14\n" + + "\x05vcpus\x18\x02 \x01(\x05R\x05vcpus\x12\x1b\n" + + "\tmemory_mb\x18\x03 \x01(\x03R\bmemoryMb\x121\n" + + "\x05disks\x18\x04 \x03(\v2\x1b.persys.agent.v1.DiskConfigR\x05disks\x12:\n" + + "\bnetworks\x18\x05 \x03(\v2\x1e.persys.agent.v1.NetworkConfigR\bnetworks\x12\x1d\n" + + "\n" + + "cloud_init\x18\x06 \x01(\tR\tcloudInit\x12A\n" + + "\bmetadata\x18\a \x03(\v2%.persys.agent.v1.VMSpec.MetadataEntryR\bmetadata\x12L\n" + + "\x11cloud_init_config\x18\b \x01(\v2 .persys.agent.v1.CloudInitConfigR\x0fcloudInitConfig\x1a;\n" + + "\rMetadataEntry\x12\x10\n" + + "\x03key\x18\x01 \x01(\tR\x03key\x12\x14\n" + + "\x05value\x18\x02 \x01(\tR\x05value:\x028\x01\"\x93\x01\n" + + "\x0fCloudInitConfig\x12\x1b\n" + + "\tuser_data\x18\x01 \x01(\tR\buserData\x12\x1b\n" + + "\tmeta_data\x18\x02 \x01(\tR\bmetaData\x12%\n" + + "\x0enetwork_config\x18\x03 \x01(\tR\rnetworkConfig\x12\x1f\n" + + "\vvendor_data\x18\x04 \x01(\tR\n" + + "vendorData\"n\n" + + "\vVolumeMount\x12\x1b\n" + + "\thost_path\x18\x01 \x01(\tR\bhostPath\x12%\n" + + "\x0econtainer_path\x18\x02 \x01(\tR\rcontainerPath\x12\x1b\n" + + "\tread_only\x18\x03 \x01(\bR\breadOnly\"m\n" + + "\vPortMapping\x12\x1b\n" + + "\thost_port\x18\x01 \x01(\x05R\bhostPort\x12%\n" + + "\x0econtainer_port\x18\x02 \x01(\x05R\rcontainerPort\x12\x1a\n" + + "\bprotocol\x18\x03 \x01(\tR\bprotocol\"~\n" + + "\x0eResourceLimits\x12\x1d\n" + + "\n" + + "cpu_shares\x18\x01 \x01(\x03R\tcpuShares\x12!\n" + + "\fmemory_bytes\x18\x02 \x01(\x03R\vmemoryBytes\x12*\n" + + "\x11memory_swap_bytes\x18\x03 \x01(\x03R\x0fmemorySwapBytes\"O\n" + + "\rRestartPolicy\x12\x16\n" + + "\x06policy\x18\x01 \x01(\tR\x06policy\x12&\n" + + "\x0fmax_retry_count\x18\x02 \x01(\x05R\rmaxRetryCount\"\x91\x01\n" + + "\n" + + "DiskConfig\x12\x12\n" + + "\x04path\x18\x01 \x01(\tR\x04path\x12\x16\n" + + "\x06device\x18\x02 \x01(\tR\x06device\x12\x16\n" + + "\x06format\x18\x03 \x01(\tR\x06format\x12\x17\n" + + "\asize_gb\x18\x04 \x01(\x03R\x06sizeGb\x12\x12\n" + + "\x04type\x18\x05 \x01(\tR\x04type\x12\x12\n" + + "\x04boot\x18\x06 \x01(\bR\x04boot\"i\n" + + "\rNetworkConfig\x12\x18\n" + + "\anetwork\x18\x01 \x01(\tR\anetwork\x12\x1f\n" + + "\vmac_address\x18\x02 \x01(\tR\n" + + "macAddress\x12\x1d\n" + + "\n" + + "ip_address\x18\x03 \x01(\tR\tipAddress\"\xd9\x03\n" + + "\x0eWorkloadStatus\x12\x0e\n" + + "\x02id\x18\x01 \x01(\tR\x02id\x121\n" + + "\x04type\x18\x02 \x01(\x0e2\x1d.persys.agent.v1.WorkloadTypeR\x04type\x12\x1f\n" + + "\vrevision_id\x18\x03 \x01(\tR\n" + + "revisionId\x12B\n" + + "\rdesired_state\x18\x04 \x01(\x0e2\x1d.persys.agent.v1.DesiredStateR\fdesiredState\x12?\n" + + "\factual_state\x18\x05 \x01(\x0e2\x1c.persys.agent.v1.ActualStateR\vactualState\x12\x18\n" + + "\amessage\x18\x06 \x01(\tR\amessage\x12\x1d\n" + + "\n" + + "created_at\x18\a \x01(\x03R\tcreatedAt\x12\x1d\n" + + "\n" + + "updated_at\x18\b \x01(\x03R\tupdatedAt\x12I\n" + + "\bmetadata\x18\t \x03(\v2-.persys.agent.v1.WorkloadStatus.MetadataEntryR\bmetadata\x1a;\n" + + "\rMetadataEntry\x12\x10\n" + + "\x03key\x18\x01 \x01(\tR\x03key\x12\x14\n" + + "\x05value\x18\x02 \x01(\tR\x05value:\x028\x01*{\n" + + "\fWorkloadType\x12\x1d\n" + + "\x19WORKLOAD_TYPE_UNSPECIFIED\x10\x00\x12\x1b\n" + + "\x17WORKLOAD_TYPE_CONTAINER\x10\x01\x12\x19\n" + + "\x15WORKLOAD_TYPE_COMPOSE\x10\x02\x12\x14\n" + + "\x10WORKLOAD_TYPE_VM\x10\x03*c\n" + + "\fDesiredState\x12\x1d\n" + + "\x19DESIRED_STATE_UNSPECIFIED\x10\x00\x12\x19\n" + + "\x15DESIRED_STATE_RUNNING\x10\x01\x12\x19\n" + + "\x15DESIRED_STATE_STOPPED\x10\x02*\xac\x01\n" + + "\vActualState\x12\x1c\n" + + "\x18ACTUAL_STATE_UNSPECIFIED\x10\x00\x12\x18\n" + + "\x14ACTUAL_STATE_PENDING\x10\x01\x12\x18\n" + + "\x14ACTUAL_STATE_RUNNING\x10\x02\x12\x18\n" + + "\x14ACTUAL_STATE_STOPPED\x10\x03\x12\x17\n" + + "\x13ACTUAL_STATE_FAILED\x10\x04\x12\x18\n" + + "\x14ACTUAL_STATE_UNKNOWN\x10\x052\xd1\x04\n" + + "\fAgentService\x12^\n" + + "\rApplyWorkload\x12%.persys.agent.v1.ApplyWorkloadRequest\x1a&.persys.agent.v1.ApplyWorkloadResponse\x12a\n" + + "\x0eDeleteWorkload\x12&.persys.agent.v1.DeleteWorkloadRequest\x1a'.persys.agent.v1.DeleteWorkloadResponse\x12j\n" + + "\x11GetWorkloadStatus\x12).persys.agent.v1.GetWorkloadStatusRequest\x1a*.persys.agent.v1.GetWorkloadStatusResponse\x12^\n" + + "\rListWorkloads\x12%.persys.agent.v1.ListWorkloadsRequest\x1a&.persys.agent.v1.ListWorkloadsResponse\x12X\n" + + "\vHealthCheck\x12#.persys.agent.v1.HealthCheckRequest\x1a$.persys.agent.v1.HealthCheckResponse\x12X\n" + + "\vListActions\x12#.persys.agent.v1.ListActionsRequest\x1a$.persys.agent.v1.ListActionsResponseBNZLgithub.com/persys-dev/persys-cloud/persys-scheduler/internal/agentpb;agentpbb\x06proto3" + +var ( + file_agent_proto_rawDescOnce sync.Once + file_agent_proto_rawDescData []byte +) + +func file_agent_proto_rawDescGZIP() []byte { + file_agent_proto_rawDescOnce.Do(func() { + file_agent_proto_rawDescData = protoimpl.X.CompressGZIP(unsafe.Slice(unsafe.StringData(file_agent_proto_rawDesc), len(file_agent_proto_rawDesc))) + }) + return file_agent_proto_rawDescData +} + +var file_agent_proto_enumTypes = make([]protoimpl.EnumInfo, 3) +var file_agent_proto_msgTypes = make([]protoimpl.MessageInfo, 31) +var file_agent_proto_goTypes = []any{ + (WorkloadType)(0), // 0: persys.agent.v1.WorkloadType + (DesiredState)(0), // 1: persys.agent.v1.DesiredState + (ActualState)(0), // 2: persys.agent.v1.ActualState + (*ApplyWorkloadRequest)(nil), // 3: persys.agent.v1.ApplyWorkloadRequest + (*ApplyWorkloadResponse)(nil), // 4: persys.agent.v1.ApplyWorkloadResponse + (*DeleteWorkloadRequest)(nil), // 5: persys.agent.v1.DeleteWorkloadRequest + (*DeleteWorkloadResponse)(nil), // 6: persys.agent.v1.DeleteWorkloadResponse + (*GetWorkloadStatusRequest)(nil), // 7: persys.agent.v1.GetWorkloadStatusRequest + (*GetWorkloadStatusResponse)(nil), // 8: persys.agent.v1.GetWorkloadStatusResponse + (*ListWorkloadsRequest)(nil), // 9: persys.agent.v1.ListWorkloadsRequest + (*ListWorkloadsResponse)(nil), // 10: persys.agent.v1.ListWorkloadsResponse + (*HealthCheckRequest)(nil), // 11: persys.agent.v1.HealthCheckRequest + (*HealthCheckResponse)(nil), // 12: persys.agent.v1.HealthCheckResponse + (*ListActionsRequest)(nil), // 13: persys.agent.v1.ListActionsRequest + (*AgentAction)(nil), // 14: persys.agent.v1.AgentAction + (*ListActionsResponse)(nil), // 15: persys.agent.v1.ListActionsResponse + (*WorkloadSpec)(nil), // 16: persys.agent.v1.WorkloadSpec + (*ContainerSpec)(nil), // 17: persys.agent.v1.ContainerSpec + (*ComposeSpec)(nil), // 18: persys.agent.v1.ComposeSpec + (*VMSpec)(nil), // 19: persys.agent.v1.VMSpec + (*CloudInitConfig)(nil), // 20: persys.agent.v1.CloudInitConfig + (*VolumeMount)(nil), // 21: persys.agent.v1.VolumeMount + (*PortMapping)(nil), // 22: persys.agent.v1.PortMapping + (*ResourceLimits)(nil), // 23: persys.agent.v1.ResourceLimits + (*RestartPolicy)(nil), // 24: persys.agent.v1.RestartPolicy + (*DiskConfig)(nil), // 25: persys.agent.v1.DiskConfig + (*NetworkConfig)(nil), // 26: persys.agent.v1.NetworkConfig + (*WorkloadStatus)(nil), // 27: persys.agent.v1.WorkloadStatus + nil, // 28: persys.agent.v1.HealthCheckResponse.RuntimeStatusEntry + nil, // 29: persys.agent.v1.ContainerSpec.EnvEntry + nil, // 30: persys.agent.v1.ContainerSpec.LabelsEntry + nil, // 31: persys.agent.v1.ComposeSpec.EnvEntry + nil, // 32: persys.agent.v1.VMSpec.MetadataEntry + nil, // 33: persys.agent.v1.WorkloadStatus.MetadataEntry +} +var file_agent_proto_depIdxs = []int32{ + 0, // 0: persys.agent.v1.ApplyWorkloadRequest.type:type_name -> persys.agent.v1.WorkloadType + 1, // 1: persys.agent.v1.ApplyWorkloadRequest.desired_state:type_name -> persys.agent.v1.DesiredState + 16, // 2: persys.agent.v1.ApplyWorkloadRequest.spec:type_name -> persys.agent.v1.WorkloadSpec + 27, // 3: persys.agent.v1.ApplyWorkloadResponse.status:type_name -> persys.agent.v1.WorkloadStatus + 27, // 4: persys.agent.v1.GetWorkloadStatusResponse.status:type_name -> persys.agent.v1.WorkloadStatus + 0, // 5: persys.agent.v1.ListWorkloadsRequest.type:type_name -> persys.agent.v1.WorkloadType + 27, // 6: persys.agent.v1.ListWorkloadsResponse.workloads:type_name -> persys.agent.v1.WorkloadStatus + 28, // 7: persys.agent.v1.HealthCheckResponse.runtime_status:type_name -> persys.agent.v1.HealthCheckResponse.RuntimeStatusEntry + 14, // 8: persys.agent.v1.ListActionsResponse.actions:type_name -> persys.agent.v1.AgentAction + 17, // 9: persys.agent.v1.WorkloadSpec.container:type_name -> persys.agent.v1.ContainerSpec + 18, // 10: persys.agent.v1.WorkloadSpec.compose:type_name -> persys.agent.v1.ComposeSpec + 19, // 11: persys.agent.v1.WorkloadSpec.vm:type_name -> persys.agent.v1.VMSpec + 29, // 12: persys.agent.v1.ContainerSpec.env:type_name -> persys.agent.v1.ContainerSpec.EnvEntry + 21, // 13: persys.agent.v1.ContainerSpec.volumes:type_name -> persys.agent.v1.VolumeMount + 22, // 14: persys.agent.v1.ContainerSpec.ports:type_name -> persys.agent.v1.PortMapping + 23, // 15: persys.agent.v1.ContainerSpec.resources:type_name -> persys.agent.v1.ResourceLimits + 24, // 16: persys.agent.v1.ContainerSpec.restart_policy:type_name -> persys.agent.v1.RestartPolicy + 30, // 17: persys.agent.v1.ContainerSpec.labels:type_name -> persys.agent.v1.ContainerSpec.LabelsEntry + 31, // 18: persys.agent.v1.ComposeSpec.env:type_name -> persys.agent.v1.ComposeSpec.EnvEntry + 25, // 19: persys.agent.v1.VMSpec.disks:type_name -> persys.agent.v1.DiskConfig + 26, // 20: persys.agent.v1.VMSpec.networks:type_name -> persys.agent.v1.NetworkConfig + 32, // 21: persys.agent.v1.VMSpec.metadata:type_name -> persys.agent.v1.VMSpec.MetadataEntry + 20, // 22: persys.agent.v1.VMSpec.cloud_init_config:type_name -> persys.agent.v1.CloudInitConfig + 0, // 23: persys.agent.v1.WorkloadStatus.type:type_name -> persys.agent.v1.WorkloadType + 1, // 24: persys.agent.v1.WorkloadStatus.desired_state:type_name -> persys.agent.v1.DesiredState + 2, // 25: persys.agent.v1.WorkloadStatus.actual_state:type_name -> persys.agent.v1.ActualState + 33, // 26: persys.agent.v1.WorkloadStatus.metadata:type_name -> persys.agent.v1.WorkloadStatus.MetadataEntry + 3, // 27: persys.agent.v1.AgentService.ApplyWorkload:input_type -> persys.agent.v1.ApplyWorkloadRequest + 5, // 28: persys.agent.v1.AgentService.DeleteWorkload:input_type -> persys.agent.v1.DeleteWorkloadRequest + 7, // 29: persys.agent.v1.AgentService.GetWorkloadStatus:input_type -> persys.agent.v1.GetWorkloadStatusRequest + 9, // 30: persys.agent.v1.AgentService.ListWorkloads:input_type -> persys.agent.v1.ListWorkloadsRequest + 11, // 31: persys.agent.v1.AgentService.HealthCheck:input_type -> persys.agent.v1.HealthCheckRequest + 13, // 32: persys.agent.v1.AgentService.ListActions:input_type -> persys.agent.v1.ListActionsRequest + 4, // 33: persys.agent.v1.AgentService.ApplyWorkload:output_type -> persys.agent.v1.ApplyWorkloadResponse + 6, // 34: persys.agent.v1.AgentService.DeleteWorkload:output_type -> persys.agent.v1.DeleteWorkloadResponse + 8, // 35: persys.agent.v1.AgentService.GetWorkloadStatus:output_type -> persys.agent.v1.GetWorkloadStatusResponse + 10, // 36: persys.agent.v1.AgentService.ListWorkloads:output_type -> persys.agent.v1.ListWorkloadsResponse + 12, // 37: persys.agent.v1.AgentService.HealthCheck:output_type -> persys.agent.v1.HealthCheckResponse + 15, // 38: persys.agent.v1.AgentService.ListActions:output_type -> persys.agent.v1.ListActionsResponse + 33, // [33:39] is the sub-list for method output_type + 27, // [27:33] is the sub-list for method input_type + 27, // [27:27] is the sub-list for extension type_name + 27, // [27:27] is the sub-list for extension extendee + 0, // [0:27] is the sub-list for field type_name +} + +func init() { file_agent_proto_init() } +func file_agent_proto_init() { + if File_agent_proto != nil { + return + } + file_agent_proto_msgTypes[13].OneofWrappers = []any{ + (*WorkloadSpec_Container)(nil), + (*WorkloadSpec_Compose)(nil), + (*WorkloadSpec_Vm)(nil), + } + type x struct{} + out := protoimpl.TypeBuilder{ + File: protoimpl.DescBuilder{ + GoPackagePath: reflect.TypeOf(x{}).PkgPath(), + RawDescriptor: unsafe.Slice(unsafe.StringData(file_agent_proto_rawDesc), len(file_agent_proto_rawDesc)), + NumEnums: 3, + NumMessages: 31, + NumExtensions: 0, + NumServices: 1, + }, + GoTypes: file_agent_proto_goTypes, + DependencyIndexes: file_agent_proto_depIdxs, + EnumInfos: file_agent_proto_enumTypes, + MessageInfos: file_agent_proto_msgTypes, + }.Build() + File_agent_proto = out.File + file_agent_proto_goTypes = nil + file_agent_proto_depIdxs = nil +} diff --git a/persys-scheduler/internal/agentpb/agent_grpc.pb.go b/persys-scheduler/internal/agentpb/agent_grpc.pb.go new file mode 100644 index 0000000..eff89c8 --- /dev/null +++ b/persys-scheduler/internal/agentpb/agent_grpc.pb.go @@ -0,0 +1,327 @@ +// Code generated by protoc-gen-go-grpc. DO NOT EDIT. +// versions: +// - protoc-gen-go-grpc v1.6.1 +// - protoc v3.21.12 +// source: agent.proto + +package agentpb + +import ( + context "context" + grpc "google.golang.org/grpc" + codes "google.golang.org/grpc/codes" + status "google.golang.org/grpc/status" +) + +// This is a compile-time assertion to ensure that this generated file +// is compatible with the grpc package it is being compiled against. +// Requires gRPC-Go v1.64.0 or later. +const _ = grpc.SupportPackageIsVersion9 + +const ( + AgentService_ApplyWorkload_FullMethodName = "/persys.agent.v1.AgentService/ApplyWorkload" + AgentService_DeleteWorkload_FullMethodName = "/persys.agent.v1.AgentService/DeleteWorkload" + AgentService_GetWorkloadStatus_FullMethodName = "/persys.agent.v1.AgentService/GetWorkloadStatus" + AgentService_ListWorkloads_FullMethodName = "/persys.agent.v1.AgentService/ListWorkloads" + AgentService_HealthCheck_FullMethodName = "/persys.agent.v1.AgentService/HealthCheck" + AgentService_ListActions_FullMethodName = "/persys.agent.v1.AgentService/ListActions" +) + +// AgentServiceClient is the client API for AgentService service. +// +// For semantics around ctx use and closing/ending streaming RPCs, please refer to https://pkg.go.dev/google.golang.org/grpc/?tab=doc#ClientConn.NewStream. +// +// AgentService defines the gRPC interface for workload management +type AgentServiceClient interface { + // ApplyWorkload creates or updates a workload + ApplyWorkload(ctx context.Context, in *ApplyWorkloadRequest, opts ...grpc.CallOption) (*ApplyWorkloadResponse, error) + // DeleteWorkload removes a workload + DeleteWorkload(ctx context.Context, in *DeleteWorkloadRequest, opts ...grpc.CallOption) (*DeleteWorkloadResponse, error) + // GetWorkloadStatus retrieves current workload status + GetWorkloadStatus(ctx context.Context, in *GetWorkloadStatusRequest, opts ...grpc.CallOption) (*GetWorkloadStatusResponse, error) + // ListWorkloads returns all managed workloads + ListWorkloads(ctx context.Context, in *ListWorkloadsRequest, opts ...grpc.CallOption) (*ListWorkloadsResponse, error) + // HealthCheck returns agent health status + HealthCheck(ctx context.Context, in *HealthCheckRequest, opts ...grpc.CallOption) (*HealthCheckResponse, error) + // ListActions returns action/task history tracked by the agent since startup + ListActions(ctx context.Context, in *ListActionsRequest, opts ...grpc.CallOption) (*ListActionsResponse, error) +} + +type agentServiceClient struct { + cc grpc.ClientConnInterface +} + +func NewAgentServiceClient(cc grpc.ClientConnInterface) AgentServiceClient { + return &agentServiceClient{cc} +} + +func (c *agentServiceClient) ApplyWorkload(ctx context.Context, in *ApplyWorkloadRequest, opts ...grpc.CallOption) (*ApplyWorkloadResponse, error) { + cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...) + out := new(ApplyWorkloadResponse) + err := c.cc.Invoke(ctx, AgentService_ApplyWorkload_FullMethodName, in, out, cOpts...) + if err != nil { + return nil, err + } + return out, nil +} + +func (c *agentServiceClient) DeleteWorkload(ctx context.Context, in *DeleteWorkloadRequest, opts ...grpc.CallOption) (*DeleteWorkloadResponse, error) { + cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...) + out := new(DeleteWorkloadResponse) + err := c.cc.Invoke(ctx, AgentService_DeleteWorkload_FullMethodName, in, out, cOpts...) + if err != nil { + return nil, err + } + return out, nil +} + +func (c *agentServiceClient) GetWorkloadStatus(ctx context.Context, in *GetWorkloadStatusRequest, opts ...grpc.CallOption) (*GetWorkloadStatusResponse, error) { + cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...) + out := new(GetWorkloadStatusResponse) + err := c.cc.Invoke(ctx, AgentService_GetWorkloadStatus_FullMethodName, in, out, cOpts...) + if err != nil { + return nil, err + } + return out, nil +} + +func (c *agentServiceClient) ListWorkloads(ctx context.Context, in *ListWorkloadsRequest, opts ...grpc.CallOption) (*ListWorkloadsResponse, error) { + cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...) + out := new(ListWorkloadsResponse) + err := c.cc.Invoke(ctx, AgentService_ListWorkloads_FullMethodName, in, out, cOpts...) + if err != nil { + return nil, err + } + return out, nil +} + +func (c *agentServiceClient) HealthCheck(ctx context.Context, in *HealthCheckRequest, opts ...grpc.CallOption) (*HealthCheckResponse, error) { + cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...) + out := new(HealthCheckResponse) + err := c.cc.Invoke(ctx, AgentService_HealthCheck_FullMethodName, in, out, cOpts...) + if err != nil { + return nil, err + } + return out, nil +} + +func (c *agentServiceClient) ListActions(ctx context.Context, in *ListActionsRequest, opts ...grpc.CallOption) (*ListActionsResponse, error) { + cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...) + out := new(ListActionsResponse) + err := c.cc.Invoke(ctx, AgentService_ListActions_FullMethodName, in, out, cOpts...) + if err != nil { + return nil, err + } + return out, nil +} + +// AgentServiceServer is the server API for AgentService service. +// All implementations must embed UnimplementedAgentServiceServer +// for forward compatibility. +// +// AgentService defines the gRPC interface for workload management +type AgentServiceServer interface { + // ApplyWorkload creates or updates a workload + ApplyWorkload(context.Context, *ApplyWorkloadRequest) (*ApplyWorkloadResponse, error) + // DeleteWorkload removes a workload + DeleteWorkload(context.Context, *DeleteWorkloadRequest) (*DeleteWorkloadResponse, error) + // GetWorkloadStatus retrieves current workload status + GetWorkloadStatus(context.Context, *GetWorkloadStatusRequest) (*GetWorkloadStatusResponse, error) + // ListWorkloads returns all managed workloads + ListWorkloads(context.Context, *ListWorkloadsRequest) (*ListWorkloadsResponse, error) + // HealthCheck returns agent health status + HealthCheck(context.Context, *HealthCheckRequest) (*HealthCheckResponse, error) + // ListActions returns action/task history tracked by the agent since startup + ListActions(context.Context, *ListActionsRequest) (*ListActionsResponse, error) + mustEmbedUnimplementedAgentServiceServer() +} + +// UnimplementedAgentServiceServer must be embedded to have +// forward compatible implementations. +// +// NOTE: this should be embedded by value instead of pointer to avoid a nil +// pointer dereference when methods are called. +type UnimplementedAgentServiceServer struct{} + +func (UnimplementedAgentServiceServer) ApplyWorkload(context.Context, *ApplyWorkloadRequest) (*ApplyWorkloadResponse, error) { + return nil, status.Error(codes.Unimplemented, "method ApplyWorkload not implemented") +} +func (UnimplementedAgentServiceServer) DeleteWorkload(context.Context, *DeleteWorkloadRequest) (*DeleteWorkloadResponse, error) { + return nil, status.Error(codes.Unimplemented, "method DeleteWorkload not implemented") +} +func (UnimplementedAgentServiceServer) GetWorkloadStatus(context.Context, *GetWorkloadStatusRequest) (*GetWorkloadStatusResponse, error) { + return nil, status.Error(codes.Unimplemented, "method GetWorkloadStatus not implemented") +} +func (UnimplementedAgentServiceServer) ListWorkloads(context.Context, *ListWorkloadsRequest) (*ListWorkloadsResponse, error) { + return nil, status.Error(codes.Unimplemented, "method ListWorkloads not implemented") +} +func (UnimplementedAgentServiceServer) HealthCheck(context.Context, *HealthCheckRequest) (*HealthCheckResponse, error) { + return nil, status.Error(codes.Unimplemented, "method HealthCheck not implemented") +} +func (UnimplementedAgentServiceServer) ListActions(context.Context, *ListActionsRequest) (*ListActionsResponse, error) { + return nil, status.Error(codes.Unimplemented, "method ListActions not implemented") +} +func (UnimplementedAgentServiceServer) mustEmbedUnimplementedAgentServiceServer() {} +func (UnimplementedAgentServiceServer) testEmbeddedByValue() {} + +// UnsafeAgentServiceServer may be embedded to opt out of forward compatibility for this service. +// Use of this interface is not recommended, as added methods to AgentServiceServer will +// result in compilation errors. +type UnsafeAgentServiceServer interface { + mustEmbedUnimplementedAgentServiceServer() +} + +func RegisterAgentServiceServer(s grpc.ServiceRegistrar, srv AgentServiceServer) { + // If the following call panics, it indicates UnimplementedAgentServiceServer was + // embedded by pointer and is nil. This will cause panics if an + // unimplemented method is ever invoked, so we test this at initialization + // time to prevent it from happening at runtime later due to I/O. + if t, ok := srv.(interface{ testEmbeddedByValue() }); ok { + t.testEmbeddedByValue() + } + s.RegisterService(&AgentService_ServiceDesc, srv) +} + +func _AgentService_ApplyWorkload_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { + in := new(ApplyWorkloadRequest) + if err := dec(in); err != nil { + return nil, err + } + if interceptor == nil { + return srv.(AgentServiceServer).ApplyWorkload(ctx, in) + } + info := &grpc.UnaryServerInfo{ + Server: srv, + FullMethod: AgentService_ApplyWorkload_FullMethodName, + } + handler := func(ctx context.Context, req interface{}) (interface{}, error) { + return srv.(AgentServiceServer).ApplyWorkload(ctx, req.(*ApplyWorkloadRequest)) + } + return interceptor(ctx, in, info, handler) +} + +func _AgentService_DeleteWorkload_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { + in := new(DeleteWorkloadRequest) + if err := dec(in); err != nil { + return nil, err + } + if interceptor == nil { + return srv.(AgentServiceServer).DeleteWorkload(ctx, in) + } + info := &grpc.UnaryServerInfo{ + Server: srv, + FullMethod: AgentService_DeleteWorkload_FullMethodName, + } + handler := func(ctx context.Context, req interface{}) (interface{}, error) { + return srv.(AgentServiceServer).DeleteWorkload(ctx, req.(*DeleteWorkloadRequest)) + } + return interceptor(ctx, in, info, handler) +} + +func _AgentService_GetWorkloadStatus_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { + in := new(GetWorkloadStatusRequest) + if err := dec(in); err != nil { + return nil, err + } + if interceptor == nil { + return srv.(AgentServiceServer).GetWorkloadStatus(ctx, in) + } + info := &grpc.UnaryServerInfo{ + Server: srv, + FullMethod: AgentService_GetWorkloadStatus_FullMethodName, + } + handler := func(ctx context.Context, req interface{}) (interface{}, error) { + return srv.(AgentServiceServer).GetWorkloadStatus(ctx, req.(*GetWorkloadStatusRequest)) + } + return interceptor(ctx, in, info, handler) +} + +func _AgentService_ListWorkloads_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { + in := new(ListWorkloadsRequest) + if err := dec(in); err != nil { + return nil, err + } + if interceptor == nil { + return srv.(AgentServiceServer).ListWorkloads(ctx, in) + } + info := &grpc.UnaryServerInfo{ + Server: srv, + FullMethod: AgentService_ListWorkloads_FullMethodName, + } + handler := func(ctx context.Context, req interface{}) (interface{}, error) { + return srv.(AgentServiceServer).ListWorkloads(ctx, req.(*ListWorkloadsRequest)) + } + return interceptor(ctx, in, info, handler) +} + +func _AgentService_HealthCheck_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { + in := new(HealthCheckRequest) + if err := dec(in); err != nil { + return nil, err + } + if interceptor == nil { + return srv.(AgentServiceServer).HealthCheck(ctx, in) + } + info := &grpc.UnaryServerInfo{ + Server: srv, + FullMethod: AgentService_HealthCheck_FullMethodName, + } + handler := func(ctx context.Context, req interface{}) (interface{}, error) { + return srv.(AgentServiceServer).HealthCheck(ctx, req.(*HealthCheckRequest)) + } + return interceptor(ctx, in, info, handler) +} + +func _AgentService_ListActions_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { + in := new(ListActionsRequest) + if err := dec(in); err != nil { + return nil, err + } + if interceptor == nil { + return srv.(AgentServiceServer).ListActions(ctx, in) + } + info := &grpc.UnaryServerInfo{ + Server: srv, + FullMethod: AgentService_ListActions_FullMethodName, + } + handler := func(ctx context.Context, req interface{}) (interface{}, error) { + return srv.(AgentServiceServer).ListActions(ctx, req.(*ListActionsRequest)) + } + return interceptor(ctx, in, info, handler) +} + +// AgentService_ServiceDesc is the grpc.ServiceDesc for AgentService service. +// It's only intended for direct use with grpc.RegisterService, +// and not to be introspected or modified (even as a copy) +var AgentService_ServiceDesc = grpc.ServiceDesc{ + ServiceName: "persys.agent.v1.AgentService", + HandlerType: (*AgentServiceServer)(nil), + Methods: []grpc.MethodDesc{ + { + MethodName: "ApplyWorkload", + Handler: _AgentService_ApplyWorkload_Handler, + }, + { + MethodName: "DeleteWorkload", + Handler: _AgentService_DeleteWorkload_Handler, + }, + { + MethodName: "GetWorkloadStatus", + Handler: _AgentService_GetWorkloadStatus_Handler, + }, + { + MethodName: "ListWorkloads", + Handler: _AgentService_ListWorkloads_Handler, + }, + { + MethodName: "HealthCheck", + Handler: _AgentService_HealthCheck_Handler, + }, + { + MethodName: "ListActions", + Handler: _AgentService_ListActions_Handler, + }, + }, + Streams: []grpc.StreamDesc{}, + Metadata: "agent.proto", +} diff --git a/prow/internal/auth/certmanager.go b/persys-scheduler/internal/auth/certmanager.go similarity index 100% rename from prow/internal/auth/certmanager.go rename to persys-scheduler/internal/auth/certmanager.go diff --git a/persys-scheduler/internal/controlv1/control.pb.go b/persys-scheduler/internal/controlv1/control.pb.go new file mode 100644 index 0000000..5cc820d --- /dev/null +++ b/persys-scheduler/internal/controlv1/control.pb.go @@ -0,0 +1,3003 @@ +// Code generated by protoc-gen-go. DO NOT EDIT. +// versions: +// protoc-gen-go v1.36.11 +// protoc v3.21.12 +// source: control.proto + +package controlv1 + +import ( + protoreflect "google.golang.org/protobuf/reflect/protoreflect" + protoimpl "google.golang.org/protobuf/runtime/protoimpl" + timestamppb "google.golang.org/protobuf/types/known/timestamppb" + reflect "reflect" + sync "sync" + unsafe "unsafe" +) + +const ( + // Verify that this generated code is sufficiently up-to-date. + _ = protoimpl.EnforceVersion(20 - protoimpl.MinVersion) + // Verify that runtime/protoimpl is sufficiently up-to-date. + _ = protoimpl.EnforceVersion(protoimpl.MaxVersion - 20) +) + +type FailureReason int32 + +const ( + FailureReason_FAILURE_REASON_UNSPECIFIED FailureReason = 0 + FailureReason_IMAGE_PULL_FAILED FailureReason = 1 + FailureReason_IMAGE_NOT_FOUND FailureReason = 2 + FailureReason_INSUFFICIENT_RESOURCES FailureReason = 3 + FailureReason_INVALID_SPEC FailureReason = 4 + FailureReason_RUNTIME_ERROR FailureReason = 5 + FailureReason_NETWORK_ERROR FailureReason = 6 + FailureReason_STORAGE_ERROR FailureReason = 7 + FailureReason_VM_BOOT_FAILED FailureReason = 8 +) + +// Enum value maps for FailureReason. +var ( + FailureReason_name = map[int32]string{ + 0: "FAILURE_REASON_UNSPECIFIED", + 1: "IMAGE_PULL_FAILED", + 2: "IMAGE_NOT_FOUND", + 3: "INSUFFICIENT_RESOURCES", + 4: "INVALID_SPEC", + 5: "RUNTIME_ERROR", + 6: "NETWORK_ERROR", + 7: "STORAGE_ERROR", + 8: "VM_BOOT_FAILED", + } + FailureReason_value = map[string]int32{ + "FAILURE_REASON_UNSPECIFIED": 0, + "IMAGE_PULL_FAILED": 1, + "IMAGE_NOT_FOUND": 2, + "INSUFFICIENT_RESOURCES": 3, + "INVALID_SPEC": 4, + "RUNTIME_ERROR": 5, + "NETWORK_ERROR": 6, + "STORAGE_ERROR": 7, + "VM_BOOT_FAILED": 8, + } +) + +func (x FailureReason) Enum() *FailureReason { + p := new(FailureReason) + *p = x + return p +} + +func (x FailureReason) String() string { + return protoimpl.X.EnumStringOf(x.Descriptor(), protoreflect.EnumNumber(x)) +} + +func (FailureReason) Descriptor() protoreflect.EnumDescriptor { + return file_control_proto_enumTypes[0].Descriptor() +} + +func (FailureReason) Type() protoreflect.EnumType { + return &file_control_proto_enumTypes[0] +} + +func (x FailureReason) Number() protoreflect.EnumNumber { + return protoreflect.EnumNumber(x) +} + +// Deprecated: Use FailureReason.Descriptor instead. +func (FailureReason) EnumDescriptor() ([]byte, []int) { + return file_control_proto_rawDescGZIP(), []int{0} +} + +type RegisterNodeRequest struct { + state protoimpl.MessageState `protogen:"open.v1"` + NodeId string `protobuf:"bytes,1,opt,name=node_id,json=nodeId,proto3" json:"node_id,omitempty"` + Capabilities *NodeCapabilities `protobuf:"bytes,2,opt,name=capabilities,proto3" json:"capabilities,omitempty"` + Labels map[string]string `protobuf:"bytes,3,rep,name=labels,proto3" json:"labels,omitempty" protobuf_key:"bytes,1,opt,name=key" protobuf_val:"bytes,2,opt,name=value"` + AgentVersion string `protobuf:"bytes,4,opt,name=agent_version,json=agentVersion,proto3" json:"agent_version,omitempty"` + GrpcEndpoint string `protobuf:"bytes,5,opt,name=grpc_endpoint,json=grpcEndpoint,proto3" json:"grpc_endpoint,omitempty"` + ClusterId string `protobuf:"bytes,6,opt,name=cluster_id,json=clusterId,proto3" json:"cluster_id,omitempty"` + Timestamp *timestamppb.Timestamp `protobuf:"bytes,7,opt,name=timestamp,proto3" json:"timestamp,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *RegisterNodeRequest) Reset() { + *x = RegisterNodeRequest{} + mi := &file_control_proto_msgTypes[0] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *RegisterNodeRequest) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*RegisterNodeRequest) ProtoMessage() {} + +func (x *RegisterNodeRequest) ProtoReflect() protoreflect.Message { + mi := &file_control_proto_msgTypes[0] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use RegisterNodeRequest.ProtoReflect.Descriptor instead. +func (*RegisterNodeRequest) Descriptor() ([]byte, []int) { + return file_control_proto_rawDescGZIP(), []int{0} +} + +func (x *RegisterNodeRequest) GetNodeId() string { + if x != nil { + return x.NodeId + } + return "" +} + +func (x *RegisterNodeRequest) GetCapabilities() *NodeCapabilities { + if x != nil { + return x.Capabilities + } + return nil +} + +func (x *RegisterNodeRequest) GetLabels() map[string]string { + if x != nil { + return x.Labels + } + return nil +} + +func (x *RegisterNodeRequest) GetAgentVersion() string { + if x != nil { + return x.AgentVersion + } + return "" +} + +func (x *RegisterNodeRequest) GetGrpcEndpoint() string { + if x != nil { + return x.GrpcEndpoint + } + return "" +} + +func (x *RegisterNodeRequest) GetClusterId() string { + if x != nil { + return x.ClusterId + } + return "" +} + +func (x *RegisterNodeRequest) GetTimestamp() *timestamppb.Timestamp { + if x != nil { + return x.Timestamp + } + return nil +} + +type NodeCapabilities struct { + state protoimpl.MessageState `protogen:"open.v1"` + CpuTotalMillicores int64 `protobuf:"varint,1,opt,name=cpu_total_millicores,json=cpuTotalMillicores,proto3" json:"cpu_total_millicores,omitempty"` + MemoryTotalMb int64 `protobuf:"varint,2,opt,name=memory_total_mb,json=memoryTotalMb,proto3" json:"memory_total_mb,omitempty"` + StoragePools []*StoragePool `protobuf:"bytes,3,rep,name=storage_pools,json=storagePools,proto3" json:"storage_pools,omitempty"` + SupportedWorkloadTypes []string `protobuf:"bytes,4,rep,name=supported_workload_types,json=supportedWorkloadTypes,proto3" json:"supported_workload_types,omitempty"` // container, compose, vm + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *NodeCapabilities) Reset() { + *x = NodeCapabilities{} + mi := &file_control_proto_msgTypes[1] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *NodeCapabilities) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*NodeCapabilities) ProtoMessage() {} + +func (x *NodeCapabilities) ProtoReflect() protoreflect.Message { + mi := &file_control_proto_msgTypes[1] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use NodeCapabilities.ProtoReflect.Descriptor instead. +func (*NodeCapabilities) Descriptor() ([]byte, []int) { + return file_control_proto_rawDescGZIP(), []int{1} +} + +func (x *NodeCapabilities) GetCpuTotalMillicores() int64 { + if x != nil { + return x.CpuTotalMillicores + } + return 0 +} + +func (x *NodeCapabilities) GetMemoryTotalMb() int64 { + if x != nil { + return x.MemoryTotalMb + } + return 0 +} + +func (x *NodeCapabilities) GetStoragePools() []*StoragePool { + if x != nil { + return x.StoragePools + } + return nil +} + +func (x *NodeCapabilities) GetSupportedWorkloadTypes() []string { + if x != nil { + return x.SupportedWorkloadTypes + } + return nil +} + +type StoragePool struct { + state protoimpl.MessageState `protogen:"open.v1"` + Name string `protobuf:"bytes,1,opt,name=name,proto3" json:"name,omitempty"` + Type string `protobuf:"bytes,2,opt,name=type,proto3" json:"type,omitempty"` // local, nfs, iscsi + TotalGb int64 `protobuf:"varint,3,opt,name=total_gb,json=totalGb,proto3" json:"total_gb,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *StoragePool) Reset() { + *x = StoragePool{} + mi := &file_control_proto_msgTypes[2] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *StoragePool) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*StoragePool) ProtoMessage() {} + +func (x *StoragePool) ProtoReflect() protoreflect.Message { + mi := &file_control_proto_msgTypes[2] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use StoragePool.ProtoReflect.Descriptor instead. +func (*StoragePool) Descriptor() ([]byte, []int) { + return file_control_proto_rawDescGZIP(), []int{2} +} + +func (x *StoragePool) GetName() string { + if x != nil { + return x.Name + } + return "" +} + +func (x *StoragePool) GetType() string { + if x != nil { + return x.Type + } + return "" +} + +func (x *StoragePool) GetTotalGb() int64 { + if x != nil { + return x.TotalGb + } + return 0 +} + +type RegisterNodeResponse struct { + state protoimpl.MessageState `protogen:"open.v1"` + Accepted bool `protobuf:"varint,1,opt,name=accepted,proto3" json:"accepted,omitempty"` + Reason string `protobuf:"bytes,2,opt,name=reason,proto3" json:"reason,omitempty"` + HeartbeatIntervalSeconds int32 `protobuf:"varint,3,opt,name=heartbeat_interval_seconds,json=heartbeatIntervalSeconds,proto3" json:"heartbeat_interval_seconds,omitempty"` + LeaseExpiresAt *timestamppb.Timestamp `protobuf:"bytes,4,opt,name=lease_expires_at,json=leaseExpiresAt,proto3" json:"lease_expires_at,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *RegisterNodeResponse) Reset() { + *x = RegisterNodeResponse{} + mi := &file_control_proto_msgTypes[3] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *RegisterNodeResponse) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*RegisterNodeResponse) ProtoMessage() {} + +func (x *RegisterNodeResponse) ProtoReflect() protoreflect.Message { + mi := &file_control_proto_msgTypes[3] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use RegisterNodeResponse.ProtoReflect.Descriptor instead. +func (*RegisterNodeResponse) Descriptor() ([]byte, []int) { + return file_control_proto_rawDescGZIP(), []int{3} +} + +func (x *RegisterNodeResponse) GetAccepted() bool { + if x != nil { + return x.Accepted + } + return false +} + +func (x *RegisterNodeResponse) GetReason() string { + if x != nil { + return x.Reason + } + return "" +} + +func (x *RegisterNodeResponse) GetHeartbeatIntervalSeconds() int32 { + if x != nil { + return x.HeartbeatIntervalSeconds + } + return 0 +} + +func (x *RegisterNodeResponse) GetLeaseExpiresAt() *timestamppb.Timestamp { + if x != nil { + return x.LeaseExpiresAt + } + return nil +} + +type HeartbeatRequest struct { + state protoimpl.MessageState `protogen:"open.v1"` + NodeId string `protobuf:"bytes,1,opt,name=node_id,json=nodeId,proto3" json:"node_id,omitempty"` + Usage *NodeUsage `protobuf:"bytes,2,opt,name=usage,proto3" json:"usage,omitempty"` + WorkloadStatuses []*WorkloadStatus `protobuf:"bytes,3,rep,name=workload_statuses,json=workloadStatuses,proto3" json:"workload_statuses,omitempty"` + Timestamp *timestamppb.Timestamp `protobuf:"bytes,4,opt,name=timestamp,proto3" json:"timestamp,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *HeartbeatRequest) Reset() { + *x = HeartbeatRequest{} + mi := &file_control_proto_msgTypes[4] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *HeartbeatRequest) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*HeartbeatRequest) ProtoMessage() {} + +func (x *HeartbeatRequest) ProtoReflect() protoreflect.Message { + mi := &file_control_proto_msgTypes[4] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use HeartbeatRequest.ProtoReflect.Descriptor instead. +func (*HeartbeatRequest) Descriptor() ([]byte, []int) { + return file_control_proto_rawDescGZIP(), []int{4} +} + +func (x *HeartbeatRequest) GetNodeId() string { + if x != nil { + return x.NodeId + } + return "" +} + +func (x *HeartbeatRequest) GetUsage() *NodeUsage { + if x != nil { + return x.Usage + } + return nil +} + +func (x *HeartbeatRequest) GetWorkloadStatuses() []*WorkloadStatus { + if x != nil { + return x.WorkloadStatuses + } + return nil +} + +func (x *HeartbeatRequest) GetTimestamp() *timestamppb.Timestamp { + if x != nil { + return x.Timestamp + } + return nil +} + +type NodeUsage struct { + state protoimpl.MessageState `protogen:"open.v1"` + CpuAllocatedMillicores int64 `protobuf:"varint,1,opt,name=cpu_allocated_millicores,json=cpuAllocatedMillicores,proto3" json:"cpu_allocated_millicores,omitempty"` + CpuUsedMillicores int64 `protobuf:"varint,2,opt,name=cpu_used_millicores,json=cpuUsedMillicores,proto3" json:"cpu_used_millicores,omitempty"` + MemoryAllocatedMb int64 `protobuf:"varint,3,opt,name=memory_allocated_mb,json=memoryAllocatedMb,proto3" json:"memory_allocated_mb,omitempty"` + MemoryUsedMb int64 `protobuf:"varint,4,opt,name=memory_used_mb,json=memoryUsedMb,proto3" json:"memory_used_mb,omitempty"` + DiskAllocatedGb int64 `protobuf:"varint,5,opt,name=disk_allocated_gb,json=diskAllocatedGb,proto3" json:"disk_allocated_gb,omitempty"` + DiskUsedGb int64 `protobuf:"varint,6,opt,name=disk_used_gb,json=diskUsedGb,proto3" json:"disk_used_gb,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *NodeUsage) Reset() { + *x = NodeUsage{} + mi := &file_control_proto_msgTypes[5] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *NodeUsage) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*NodeUsage) ProtoMessage() {} + +func (x *NodeUsage) ProtoReflect() protoreflect.Message { + mi := &file_control_proto_msgTypes[5] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use NodeUsage.ProtoReflect.Descriptor instead. +func (*NodeUsage) Descriptor() ([]byte, []int) { + return file_control_proto_rawDescGZIP(), []int{5} +} + +func (x *NodeUsage) GetCpuAllocatedMillicores() int64 { + if x != nil { + return x.CpuAllocatedMillicores + } + return 0 +} + +func (x *NodeUsage) GetCpuUsedMillicores() int64 { + if x != nil { + return x.CpuUsedMillicores + } + return 0 +} + +func (x *NodeUsage) GetMemoryAllocatedMb() int64 { + if x != nil { + return x.MemoryAllocatedMb + } + return 0 +} + +func (x *NodeUsage) GetMemoryUsedMb() int64 { + if x != nil { + return x.MemoryUsedMb + } + return 0 +} + +func (x *NodeUsage) GetDiskAllocatedGb() int64 { + if x != nil { + return x.DiskAllocatedGb + } + return 0 +} + +func (x *NodeUsage) GetDiskUsedGb() int64 { + if x != nil { + return x.DiskUsedGb + } + return 0 +} + +type HeartbeatResponse struct { + state protoimpl.MessageState `protogen:"open.v1"` + Acknowledged bool `protobuf:"varint,1,opt,name=acknowledged,proto3" json:"acknowledged,omitempty"` + DrainNode bool `protobuf:"varint,2,opt,name=drain_node,json=drainNode,proto3" json:"drain_node,omitempty"` + LeaseExpiresAt *timestamppb.Timestamp `protobuf:"bytes,3,opt,name=lease_expires_at,json=leaseExpiresAt,proto3" json:"lease_expires_at,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *HeartbeatResponse) Reset() { + *x = HeartbeatResponse{} + mi := &file_control_proto_msgTypes[6] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *HeartbeatResponse) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*HeartbeatResponse) ProtoMessage() {} + +func (x *HeartbeatResponse) ProtoReflect() protoreflect.Message { + mi := &file_control_proto_msgTypes[6] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use HeartbeatResponse.ProtoReflect.Descriptor instead. +func (*HeartbeatResponse) Descriptor() ([]byte, []int) { + return file_control_proto_rawDescGZIP(), []int{6} +} + +func (x *HeartbeatResponse) GetAcknowledged() bool { + if x != nil { + return x.Acknowledged + } + return false +} + +func (x *HeartbeatResponse) GetDrainNode() bool { + if x != nil { + return x.DrainNode + } + return false +} + +func (x *HeartbeatResponse) GetLeaseExpiresAt() *timestamppb.Timestamp { + if x != nil { + return x.LeaseExpiresAt + } + return nil +} + +type ApplyWorkloadRequest struct { + state protoimpl.MessageState `protogen:"open.v1"` + WorkloadId string `protobuf:"bytes,1,opt,name=workload_id,json=workloadId,proto3" json:"workload_id,omitempty"` + Spec *WorkloadSpec `protobuf:"bytes,2,opt,name=spec,proto3" json:"spec,omitempty"` + // Compatibility fields aligned with existing agent apply semantics. + RevisionId string `protobuf:"bytes,10,opt,name=revision_id,json=revisionId,proto3" json:"revision_id,omitempty"` + DesiredState string `protobuf:"bytes,11,opt,name=desired_state,json=desiredState,proto3" json:"desired_state,omitempty"` // Running | Stopped + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *ApplyWorkloadRequest) Reset() { + *x = ApplyWorkloadRequest{} + mi := &file_control_proto_msgTypes[7] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *ApplyWorkloadRequest) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*ApplyWorkloadRequest) ProtoMessage() {} + +func (x *ApplyWorkloadRequest) ProtoReflect() protoreflect.Message { + mi := &file_control_proto_msgTypes[7] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use ApplyWorkloadRequest.ProtoReflect.Descriptor instead. +func (*ApplyWorkloadRequest) Descriptor() ([]byte, []int) { + return file_control_proto_rawDescGZIP(), []int{7} +} + +func (x *ApplyWorkloadRequest) GetWorkloadId() string { + if x != nil { + return x.WorkloadId + } + return "" +} + +func (x *ApplyWorkloadRequest) GetSpec() *WorkloadSpec { + if x != nil { + return x.Spec + } + return nil +} + +func (x *ApplyWorkloadRequest) GetRevisionId() string { + if x != nil { + return x.RevisionId + } + return "" +} + +func (x *ApplyWorkloadRequest) GetDesiredState() string { + if x != nil { + return x.DesiredState + } + return "" +} + +type ApplyWorkloadResponse struct { + state protoimpl.MessageState `protogen:"open.v1"` + Success bool `protobuf:"varint,1,opt,name=success,proto3" json:"success,omitempty"` + FailureReason FailureReason `protobuf:"varint,2,opt,name=failure_reason,json=failureReason,proto3,enum=persys.control.v1.FailureReason" json:"failure_reason,omitempty"` + ErrorMessage string `protobuf:"bytes,3,opt,name=error_message,json=errorMessage,proto3" json:"error_message,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *ApplyWorkloadResponse) Reset() { + *x = ApplyWorkloadResponse{} + mi := &file_control_proto_msgTypes[8] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *ApplyWorkloadResponse) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*ApplyWorkloadResponse) ProtoMessage() {} + +func (x *ApplyWorkloadResponse) ProtoReflect() protoreflect.Message { + mi := &file_control_proto_msgTypes[8] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use ApplyWorkloadResponse.ProtoReflect.Descriptor instead. +func (*ApplyWorkloadResponse) Descriptor() ([]byte, []int) { + return file_control_proto_rawDescGZIP(), []int{8} +} + +func (x *ApplyWorkloadResponse) GetSuccess() bool { + if x != nil { + return x.Success + } + return false +} + +func (x *ApplyWorkloadResponse) GetFailureReason() FailureReason { + if x != nil { + return x.FailureReason + } + return FailureReason_FAILURE_REASON_UNSPECIFIED +} + +func (x *ApplyWorkloadResponse) GetErrorMessage() string { + if x != nil { + return x.ErrorMessage + } + return "" +} + +type DeleteWorkloadRequest struct { + state protoimpl.MessageState `protogen:"open.v1"` + WorkloadId string `protobuf:"bytes,1,opt,name=workload_id,json=workloadId,proto3" json:"workload_id,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *DeleteWorkloadRequest) Reset() { + *x = DeleteWorkloadRequest{} + mi := &file_control_proto_msgTypes[9] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *DeleteWorkloadRequest) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*DeleteWorkloadRequest) ProtoMessage() {} + +func (x *DeleteWorkloadRequest) ProtoReflect() protoreflect.Message { + mi := &file_control_proto_msgTypes[9] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use DeleteWorkloadRequest.ProtoReflect.Descriptor instead. +func (*DeleteWorkloadRequest) Descriptor() ([]byte, []int) { + return file_control_proto_rawDescGZIP(), []int{9} +} + +func (x *DeleteWorkloadRequest) GetWorkloadId() string { + if x != nil { + return x.WorkloadId + } + return "" +} + +type DeleteWorkloadResponse struct { + state protoimpl.MessageState `protogen:"open.v1"` + Success bool `protobuf:"varint,1,opt,name=success,proto3" json:"success,omitempty"` + ErrorMessage string `protobuf:"bytes,2,opt,name=error_message,json=errorMessage,proto3" json:"error_message,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *DeleteWorkloadResponse) Reset() { + *x = DeleteWorkloadResponse{} + mi := &file_control_proto_msgTypes[10] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *DeleteWorkloadResponse) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*DeleteWorkloadResponse) ProtoMessage() {} + +func (x *DeleteWorkloadResponse) ProtoReflect() protoreflect.Message { + mi := &file_control_proto_msgTypes[10] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use DeleteWorkloadResponse.ProtoReflect.Descriptor instead. +func (*DeleteWorkloadResponse) Descriptor() ([]byte, []int) { + return file_control_proto_rawDescGZIP(), []int{10} +} + +func (x *DeleteWorkloadResponse) GetSuccess() bool { + if x != nil { + return x.Success + } + return false +} + +func (x *DeleteWorkloadResponse) GetErrorMessage() string { + if x != nil { + return x.ErrorMessage + } + return "" +} + +type WorkloadSpec struct { + state protoimpl.MessageState `protogen:"open.v1"` + Type string `protobuf:"bytes,1,opt,name=type,proto3" json:"type,omitempty"` // container, compose, vm + Resources *ResourceRequirements `protobuf:"bytes,2,opt,name=resources,proto3" json:"resources,omitempty"` + // Types that are valid to be assigned to Workload: + // + // *WorkloadSpec_Container + // *WorkloadSpec_Compose + // *WorkloadSpec_Vm + Workload isWorkloadSpec_Workload `protobuf_oneof:"workload"` + Metadata map[string]string `protobuf:"bytes,20,rep,name=metadata,proto3" json:"metadata,omitempty" protobuf_key:"bytes,1,opt,name=key" protobuf_val:"bytes,2,opt,name=value"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *WorkloadSpec) Reset() { + *x = WorkloadSpec{} + mi := &file_control_proto_msgTypes[11] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *WorkloadSpec) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*WorkloadSpec) ProtoMessage() {} + +func (x *WorkloadSpec) ProtoReflect() protoreflect.Message { + mi := &file_control_proto_msgTypes[11] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use WorkloadSpec.ProtoReflect.Descriptor instead. +func (*WorkloadSpec) Descriptor() ([]byte, []int) { + return file_control_proto_rawDescGZIP(), []int{11} +} + +func (x *WorkloadSpec) GetType() string { + if x != nil { + return x.Type + } + return "" +} + +func (x *WorkloadSpec) GetResources() *ResourceRequirements { + if x != nil { + return x.Resources + } + return nil +} + +func (x *WorkloadSpec) GetWorkload() isWorkloadSpec_Workload { + if x != nil { + return x.Workload + } + return nil +} + +func (x *WorkloadSpec) GetContainer() *ContainerSpec { + if x != nil { + if x, ok := x.Workload.(*WorkloadSpec_Container); ok { + return x.Container + } + } + return nil +} + +func (x *WorkloadSpec) GetCompose() *ComposeSpec { + if x != nil { + if x, ok := x.Workload.(*WorkloadSpec_Compose); ok { + return x.Compose + } + } + return nil +} + +func (x *WorkloadSpec) GetVm() *VMSpec { + if x != nil { + if x, ok := x.Workload.(*WorkloadSpec_Vm); ok { + return x.Vm + } + } + return nil +} + +func (x *WorkloadSpec) GetMetadata() map[string]string { + if x != nil { + return x.Metadata + } + return nil +} + +type isWorkloadSpec_Workload interface { + isWorkloadSpec_Workload() +} + +type WorkloadSpec_Container struct { + Container *ContainerSpec `protobuf:"bytes,10,opt,name=container,proto3,oneof"` +} + +type WorkloadSpec_Compose struct { + Compose *ComposeSpec `protobuf:"bytes,11,opt,name=compose,proto3,oneof"` +} + +type WorkloadSpec_Vm struct { + Vm *VMSpec `protobuf:"bytes,12,opt,name=vm,proto3,oneof"` +} + +func (*WorkloadSpec_Container) isWorkloadSpec_Workload() {} + +func (*WorkloadSpec_Compose) isWorkloadSpec_Workload() {} + +func (*WorkloadSpec_Vm) isWorkloadSpec_Workload() {} + +type ResourceRequirements struct { + state protoimpl.MessageState `protogen:"open.v1"` + CpuMillicores int64 `protobuf:"varint,1,opt,name=cpu_millicores,json=cpuMillicores,proto3" json:"cpu_millicores,omitempty"` + MemoryMb int64 `protobuf:"varint,2,opt,name=memory_mb,json=memoryMb,proto3" json:"memory_mb,omitempty"` + DiskGb int64 `protobuf:"varint,3,opt,name=disk_gb,json=diskGb,proto3" json:"disk_gb,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *ResourceRequirements) Reset() { + *x = ResourceRequirements{} + mi := &file_control_proto_msgTypes[12] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *ResourceRequirements) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*ResourceRequirements) ProtoMessage() {} + +func (x *ResourceRequirements) ProtoReflect() protoreflect.Message { + mi := &file_control_proto_msgTypes[12] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use ResourceRequirements.ProtoReflect.Descriptor instead. +func (*ResourceRequirements) Descriptor() ([]byte, []int) { + return file_control_proto_rawDescGZIP(), []int{12} +} + +func (x *ResourceRequirements) GetCpuMillicores() int64 { + if x != nil { + return x.CpuMillicores + } + return 0 +} + +func (x *ResourceRequirements) GetMemoryMb() int64 { + if x != nil { + return x.MemoryMb + } + return 0 +} + +func (x *ResourceRequirements) GetDiskGb() int64 { + if x != nil { + return x.DiskGb + } + return 0 +} + +type ContainerSpec struct { + state protoimpl.MessageState `protogen:"open.v1"` + Image string `protobuf:"bytes,1,opt,name=image,proto3" json:"image,omitempty"` + Command []string `protobuf:"bytes,2,rep,name=command,proto3" json:"command,omitempty"` + Env map[string]string `protobuf:"bytes,3,rep,name=env,proto3" json:"env,omitempty" protobuf_key:"bytes,1,opt,name=key" protobuf_val:"bytes,2,opt,name=value"` + Volumes []*VolumeMount `protobuf:"bytes,4,rep,name=volumes,proto3" json:"volumes,omitempty"` + Ports []*Port `protobuf:"bytes,5,rep,name=ports,proto3" json:"ports,omitempty"` + RestartPolicy string `protobuf:"bytes,6,opt,name=restart_policy,json=restartPolicy,proto3" json:"restart_policy,omitempty"` + Privileged bool `protobuf:"varint,7,opt,name=privileged,proto3" json:"privileged,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *ContainerSpec) Reset() { + *x = ContainerSpec{} + mi := &file_control_proto_msgTypes[13] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *ContainerSpec) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*ContainerSpec) ProtoMessage() {} + +func (x *ContainerSpec) ProtoReflect() protoreflect.Message { + mi := &file_control_proto_msgTypes[13] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use ContainerSpec.ProtoReflect.Descriptor instead. +func (*ContainerSpec) Descriptor() ([]byte, []int) { + return file_control_proto_rawDescGZIP(), []int{13} +} + +func (x *ContainerSpec) GetImage() string { + if x != nil { + return x.Image + } + return "" +} + +func (x *ContainerSpec) GetCommand() []string { + if x != nil { + return x.Command + } + return nil +} + +func (x *ContainerSpec) GetEnv() map[string]string { + if x != nil { + return x.Env + } + return nil +} + +func (x *ContainerSpec) GetVolumes() []*VolumeMount { + if x != nil { + return x.Volumes + } + return nil +} + +func (x *ContainerSpec) GetPorts() []*Port { + if x != nil { + return x.Ports + } + return nil +} + +func (x *ContainerSpec) GetRestartPolicy() string { + if x != nil { + return x.RestartPolicy + } + return "" +} + +func (x *ContainerSpec) GetPrivileged() bool { + if x != nil { + return x.Privileged + } + return false +} + +type VolumeMount struct { + state protoimpl.MessageState `protogen:"open.v1"` + HostPath string `protobuf:"bytes,1,opt,name=host_path,json=hostPath,proto3" json:"host_path,omitempty"` + ContainerPath string `protobuf:"bytes,2,opt,name=container_path,json=containerPath,proto3" json:"container_path,omitempty"` + ReadOnly bool `protobuf:"varint,3,opt,name=read_only,json=readOnly,proto3" json:"read_only,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *VolumeMount) Reset() { + *x = VolumeMount{} + mi := &file_control_proto_msgTypes[14] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *VolumeMount) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*VolumeMount) ProtoMessage() {} + +func (x *VolumeMount) ProtoReflect() protoreflect.Message { + mi := &file_control_proto_msgTypes[14] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use VolumeMount.ProtoReflect.Descriptor instead. +func (*VolumeMount) Descriptor() ([]byte, []int) { + return file_control_proto_rawDescGZIP(), []int{14} +} + +func (x *VolumeMount) GetHostPath() string { + if x != nil { + return x.HostPath + } + return "" +} + +func (x *VolumeMount) GetContainerPath() string { + if x != nil { + return x.ContainerPath + } + return "" +} + +func (x *VolumeMount) GetReadOnly() bool { + if x != nil { + return x.ReadOnly + } + return false +} + +type Port struct { + state protoimpl.MessageState `protogen:"open.v1"` + HostPort int32 `protobuf:"varint,1,opt,name=host_port,json=hostPort,proto3" json:"host_port,omitempty"` + ContainerPort int32 `protobuf:"varint,2,opt,name=container_port,json=containerPort,proto3" json:"container_port,omitempty"` + Protocol string `protobuf:"bytes,3,opt,name=protocol,proto3" json:"protocol,omitempty"` // tcp or udp + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *Port) Reset() { + *x = Port{} + mi := &file_control_proto_msgTypes[15] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *Port) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*Port) ProtoMessage() {} + +func (x *Port) ProtoReflect() protoreflect.Message { + mi := &file_control_proto_msgTypes[15] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use Port.ProtoReflect.Descriptor instead. +func (*Port) Descriptor() ([]byte, []int) { + return file_control_proto_rawDescGZIP(), []int{15} +} + +func (x *Port) GetHostPort() int32 { + if x != nil { + return x.HostPort + } + return 0 +} + +func (x *Port) GetContainerPort() int32 { + if x != nil { + return x.ContainerPort + } + return 0 +} + +func (x *Port) GetProtocol() string { + if x != nil { + return x.Protocol + } + return "" +} + +type ComposeSpec struct { + state protoimpl.MessageState `protogen:"open.v1"` + SourceType string `protobuf:"bytes,1,opt,name=source_type,json=sourceType,proto3" json:"source_type,omitempty"` // git or inline + GitRepo string `protobuf:"bytes,2,opt,name=git_repo,json=gitRepo,proto3" json:"git_repo,omitempty"` + GitRef string `protobuf:"bytes,3,opt,name=git_ref,json=gitRef,proto3" json:"git_ref,omitempty"` + InlineYaml string `protobuf:"bytes,4,opt,name=inline_yaml,json=inlineYaml,proto3" json:"inline_yaml,omitempty"` + Env map[string]string `protobuf:"bytes,5,rep,name=env,proto3" json:"env,omitempty" protobuf_key:"bytes,1,opt,name=key" protobuf_val:"bytes,2,opt,name=value"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *ComposeSpec) Reset() { + *x = ComposeSpec{} + mi := &file_control_proto_msgTypes[16] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *ComposeSpec) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*ComposeSpec) ProtoMessage() {} + +func (x *ComposeSpec) ProtoReflect() protoreflect.Message { + mi := &file_control_proto_msgTypes[16] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use ComposeSpec.ProtoReflect.Descriptor instead. +func (*ComposeSpec) Descriptor() ([]byte, []int) { + return file_control_proto_rawDescGZIP(), []int{16} +} + +func (x *ComposeSpec) GetSourceType() string { + if x != nil { + return x.SourceType + } + return "" +} + +func (x *ComposeSpec) GetGitRepo() string { + if x != nil { + return x.GitRepo + } + return "" +} + +func (x *ComposeSpec) GetGitRef() string { + if x != nil { + return x.GitRef + } + return "" +} + +func (x *ComposeSpec) GetInlineYaml() string { + if x != nil { + return x.InlineYaml + } + return "" +} + +func (x *ComposeSpec) GetEnv() map[string]string { + if x != nil { + return x.Env + } + return nil +} + +type VMSpec struct { + state protoimpl.MessageState `protogen:"open.v1"` + Vcpus int32 `protobuf:"varint,1,opt,name=vcpus,proto3" json:"vcpus,omitempty"` + MemoryMb int64 `protobuf:"varint,2,opt,name=memory_mb,json=memoryMb,proto3" json:"memory_mb,omitempty"` + Disks []*DiskConfig `protobuf:"bytes,3,rep,name=disks,proto3" json:"disks,omitempty"` + Networks []*NetworkConfig `protobuf:"bytes,4,rep,name=networks,proto3" json:"networks,omitempty"` + CloudInit *CloudInitConfig `protobuf:"bytes,5,opt,name=cloud_init,json=cloudInit,proto3" json:"cloud_init,omitempty"` + OsImage string `protobuf:"bytes,6,opt,name=os_image,json=osImage,proto3" json:"os_image,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *VMSpec) Reset() { + *x = VMSpec{} + mi := &file_control_proto_msgTypes[17] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *VMSpec) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*VMSpec) ProtoMessage() {} + +func (x *VMSpec) ProtoReflect() protoreflect.Message { + mi := &file_control_proto_msgTypes[17] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use VMSpec.ProtoReflect.Descriptor instead. +func (*VMSpec) Descriptor() ([]byte, []int) { + return file_control_proto_rawDescGZIP(), []int{17} +} + +func (x *VMSpec) GetVcpus() int32 { + if x != nil { + return x.Vcpus + } + return 0 +} + +func (x *VMSpec) GetMemoryMb() int64 { + if x != nil { + return x.MemoryMb + } + return 0 +} + +func (x *VMSpec) GetDisks() []*DiskConfig { + if x != nil { + return x.Disks + } + return nil +} + +func (x *VMSpec) GetNetworks() []*NetworkConfig { + if x != nil { + return x.Networks + } + return nil +} + +func (x *VMSpec) GetCloudInit() *CloudInitConfig { + if x != nil { + return x.CloudInit + } + return nil +} + +func (x *VMSpec) GetOsImage() string { + if x != nil { + return x.OsImage + } + return "" +} + +type DiskConfig struct { + state protoimpl.MessageState `protogen:"open.v1"` + PoolName string `protobuf:"bytes,1,opt,name=pool_name,json=poolName,proto3" json:"pool_name,omitempty"` + SizeGb int64 `protobuf:"varint,2,opt,name=size_gb,json=sizeGb,proto3" json:"size_gb,omitempty"` + MountPoint string `protobuf:"bytes,3,opt,name=mount_point,json=mountPoint,proto3" json:"mount_point,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *DiskConfig) Reset() { + *x = DiskConfig{} + mi := &file_control_proto_msgTypes[18] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *DiskConfig) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*DiskConfig) ProtoMessage() {} + +func (x *DiskConfig) ProtoReflect() protoreflect.Message { + mi := &file_control_proto_msgTypes[18] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use DiskConfig.ProtoReflect.Descriptor instead. +func (*DiskConfig) Descriptor() ([]byte, []int) { + return file_control_proto_rawDescGZIP(), []int{18} +} + +func (x *DiskConfig) GetPoolName() string { + if x != nil { + return x.PoolName + } + return "" +} + +func (x *DiskConfig) GetSizeGb() int64 { + if x != nil { + return x.SizeGb + } + return 0 +} + +func (x *DiskConfig) GetMountPoint() string { + if x != nil { + return x.MountPoint + } + return "" +} + +type NetworkConfig struct { + state protoimpl.MessageState `protogen:"open.v1"` + Bridge string `protobuf:"bytes,1,opt,name=bridge,proto3" json:"bridge,omitempty"` + Dhcp bool `protobuf:"varint,2,opt,name=dhcp,proto3" json:"dhcp,omitempty"` + StaticIp string `protobuf:"bytes,3,opt,name=static_ip,json=staticIp,proto3" json:"static_ip,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *NetworkConfig) Reset() { + *x = NetworkConfig{} + mi := &file_control_proto_msgTypes[19] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *NetworkConfig) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*NetworkConfig) ProtoMessage() {} + +func (x *NetworkConfig) ProtoReflect() protoreflect.Message { + mi := &file_control_proto_msgTypes[19] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use NetworkConfig.ProtoReflect.Descriptor instead. +func (*NetworkConfig) Descriptor() ([]byte, []int) { + return file_control_proto_rawDescGZIP(), []int{19} +} + +func (x *NetworkConfig) GetBridge() string { + if x != nil { + return x.Bridge + } + return "" +} + +func (x *NetworkConfig) GetDhcp() bool { + if x != nil { + return x.Dhcp + } + return false +} + +func (x *NetworkConfig) GetStaticIp() string { + if x != nil { + return x.StaticIp + } + return "" +} + +type CloudInitConfig struct { + state protoimpl.MessageState `protogen:"open.v1"` + UserData string `protobuf:"bytes,1,opt,name=user_data,json=userData,proto3" json:"user_data,omitempty"` + MetaData string `protobuf:"bytes,2,opt,name=meta_data,json=metaData,proto3" json:"meta_data,omitempty"` + NetworkConfig string `protobuf:"bytes,3,opt,name=network_config,json=networkConfig,proto3" json:"network_config,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *CloudInitConfig) Reset() { + *x = CloudInitConfig{} + mi := &file_control_proto_msgTypes[20] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *CloudInitConfig) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*CloudInitConfig) ProtoMessage() {} + +func (x *CloudInitConfig) ProtoReflect() protoreflect.Message { + mi := &file_control_proto_msgTypes[20] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use CloudInitConfig.ProtoReflect.Descriptor instead. +func (*CloudInitConfig) Descriptor() ([]byte, []int) { + return file_control_proto_rawDescGZIP(), []int{20} +} + +func (x *CloudInitConfig) GetUserData() string { + if x != nil { + return x.UserData + } + return "" +} + +func (x *CloudInitConfig) GetMetaData() string { + if x != nil { + return x.MetaData + } + return "" +} + +func (x *CloudInitConfig) GetNetworkConfig() string { + if x != nil { + return x.NetworkConfig + } + return "" +} + +type WorkloadStatus struct { + state protoimpl.MessageState `protogen:"open.v1"` + WorkloadId string `protobuf:"bytes,1,opt,name=workload_id,json=workloadId,proto3" json:"workload_id,omitempty"` + State string `protobuf:"bytes,2,opt,name=state,proto3" json:"state,omitempty"` // Running, Stopped, Failed + FailureReason FailureReason `protobuf:"varint,3,opt,name=failure_reason,json=failureReason,proto3,enum=persys.control.v1.FailureReason" json:"failure_reason,omitempty"` + Message string `protobuf:"bytes,4,opt,name=message,proto3" json:"message,omitempty"` + LastTransition *timestamppb.Timestamp `protobuf:"bytes,5,opt,name=last_transition,json=lastTransition,proto3" json:"last_transition,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *WorkloadStatus) Reset() { + *x = WorkloadStatus{} + mi := &file_control_proto_msgTypes[21] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *WorkloadStatus) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*WorkloadStatus) ProtoMessage() {} + +func (x *WorkloadStatus) ProtoReflect() protoreflect.Message { + mi := &file_control_proto_msgTypes[21] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use WorkloadStatus.ProtoReflect.Descriptor instead. +func (*WorkloadStatus) Descriptor() ([]byte, []int) { + return file_control_proto_rawDescGZIP(), []int{21} +} + +func (x *WorkloadStatus) GetWorkloadId() string { + if x != nil { + return x.WorkloadId + } + return "" +} + +func (x *WorkloadStatus) GetState() string { + if x != nil { + return x.State + } + return "" +} + +func (x *WorkloadStatus) GetFailureReason() FailureReason { + if x != nil { + return x.FailureReason + } + return FailureReason_FAILURE_REASON_UNSPECIFIED +} + +func (x *WorkloadStatus) GetMessage() string { + if x != nil { + return x.Message + } + return "" +} + +func (x *WorkloadStatus) GetLastTransition() *timestamppb.Timestamp { + if x != nil { + return x.LastTransition + } + return nil +} + +type RetryWorkloadRequest struct { + state protoimpl.MessageState `protogen:"open.v1"` + WorkloadId string `protobuf:"bytes,1,opt,name=workload_id,json=workloadId,proto3" json:"workload_id,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *RetryWorkloadRequest) Reset() { + *x = RetryWorkloadRequest{} + mi := &file_control_proto_msgTypes[22] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *RetryWorkloadRequest) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*RetryWorkloadRequest) ProtoMessage() {} + +func (x *RetryWorkloadRequest) ProtoReflect() protoreflect.Message { + mi := &file_control_proto_msgTypes[22] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use RetryWorkloadRequest.ProtoReflect.Descriptor instead. +func (*RetryWorkloadRequest) Descriptor() ([]byte, []int) { + return file_control_proto_rawDescGZIP(), []int{22} +} + +func (x *RetryWorkloadRequest) GetWorkloadId() string { + if x != nil { + return x.WorkloadId + } + return "" +} + +type RetryWorkloadResponse struct { + state protoimpl.MessageState `protogen:"open.v1"` + Accepted bool `protobuf:"varint,1,opt,name=accepted,proto3" json:"accepted,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *RetryWorkloadResponse) Reset() { + *x = RetryWorkloadResponse{} + mi := &file_control_proto_msgTypes[23] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *RetryWorkloadResponse) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*RetryWorkloadResponse) ProtoMessage() {} + +func (x *RetryWorkloadResponse) ProtoReflect() protoreflect.Message { + mi := &file_control_proto_msgTypes[23] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use RetryWorkloadResponse.ProtoReflect.Descriptor instead. +func (*RetryWorkloadResponse) Descriptor() ([]byte, []int) { + return file_control_proto_rawDescGZIP(), []int{23} +} + +func (x *RetryWorkloadResponse) GetAccepted() bool { + if x != nil { + return x.Accepted + } + return false +} + +type ListNodesRequest struct { + state protoimpl.MessageState `protogen:"open.v1"` + Status string `protobuf:"bytes,1,opt,name=status,proto3" json:"status,omitempty"` // optional filter: Ready | NotReady | Draining + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *ListNodesRequest) Reset() { + *x = ListNodesRequest{} + mi := &file_control_proto_msgTypes[24] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *ListNodesRequest) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*ListNodesRequest) ProtoMessage() {} + +func (x *ListNodesRequest) ProtoReflect() protoreflect.Message { + mi := &file_control_proto_msgTypes[24] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use ListNodesRequest.ProtoReflect.Descriptor instead. +func (*ListNodesRequest) Descriptor() ([]byte, []int) { + return file_control_proto_rawDescGZIP(), []int{24} +} + +func (x *ListNodesRequest) GetStatus() string { + if x != nil { + return x.Status + } + return "" +} + +type GetNodeRequest struct { + state protoimpl.MessageState `protogen:"open.v1"` + NodeId string `protobuf:"bytes,1,opt,name=node_id,json=nodeId,proto3" json:"node_id,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *GetNodeRequest) Reset() { + *x = GetNodeRequest{} + mi := &file_control_proto_msgTypes[25] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *GetNodeRequest) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*GetNodeRequest) ProtoMessage() {} + +func (x *GetNodeRequest) ProtoReflect() protoreflect.Message { + mi := &file_control_proto_msgTypes[25] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use GetNodeRequest.ProtoReflect.Descriptor instead. +func (*GetNodeRequest) Descriptor() ([]byte, []int) { + return file_control_proto_rawDescGZIP(), []int{25} +} + +func (x *GetNodeRequest) GetNodeId() string { + if x != nil { + return x.NodeId + } + return "" +} + +type ListNodesResponse struct { + state protoimpl.MessageState `protogen:"open.v1"` + Nodes []*NodeView `protobuf:"bytes,1,rep,name=nodes,proto3" json:"nodes,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *ListNodesResponse) Reset() { + *x = ListNodesResponse{} + mi := &file_control_proto_msgTypes[26] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *ListNodesResponse) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*ListNodesResponse) ProtoMessage() {} + +func (x *ListNodesResponse) ProtoReflect() protoreflect.Message { + mi := &file_control_proto_msgTypes[26] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use ListNodesResponse.ProtoReflect.Descriptor instead. +func (*ListNodesResponse) Descriptor() ([]byte, []int) { + return file_control_proto_rawDescGZIP(), []int{26} +} + +func (x *ListNodesResponse) GetNodes() []*NodeView { + if x != nil { + return x.Nodes + } + return nil +} + +type GetNodeResponse struct { + state protoimpl.MessageState `protogen:"open.v1"` + Node *NodeView `protobuf:"bytes,1,opt,name=node,proto3" json:"node,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *GetNodeResponse) Reset() { + *x = GetNodeResponse{} + mi := &file_control_proto_msgTypes[27] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *GetNodeResponse) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*GetNodeResponse) ProtoMessage() {} + +func (x *GetNodeResponse) ProtoReflect() protoreflect.Message { + mi := &file_control_proto_msgTypes[27] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use GetNodeResponse.ProtoReflect.Descriptor instead. +func (*GetNodeResponse) Descriptor() ([]byte, []int) { + return file_control_proto_rawDescGZIP(), []int{27} +} + +func (x *GetNodeResponse) GetNode() *NodeView { + if x != nil { + return x.Node + } + return nil +} + +type NodeView struct { + state protoimpl.MessageState `protogen:"open.v1"` + NodeId string `protobuf:"bytes,1,opt,name=node_id,json=nodeId,proto3" json:"node_id,omitempty"` + Status string `protobuf:"bytes,2,opt,name=status,proto3" json:"status,omitempty"` + StatusReason string `protobuf:"bytes,3,opt,name=status_reason,json=statusReason,proto3" json:"status_reason,omitempty"` + StatusUpdatedBy string `protobuf:"bytes,4,opt,name=status_updated_by,json=statusUpdatedBy,proto3" json:"status_updated_by,omitempty"` + StatusUpdatedAt *timestamppb.Timestamp `protobuf:"bytes,5,opt,name=status_updated_at,json=statusUpdatedAt,proto3" json:"status_updated_at,omitempty"` + LastHeartbeat *timestamppb.Timestamp `protobuf:"bytes,6,opt,name=last_heartbeat,json=lastHeartbeat,proto3" json:"last_heartbeat,omitempty"` + GrpcEndpoint string `protobuf:"bytes,7,opt,name=grpc_endpoint,json=grpcEndpoint,proto3" json:"grpc_endpoint,omitempty"` + TotalCpuCores float64 `protobuf:"fixed64,8,opt,name=total_cpu_cores,json=totalCpuCores,proto3" json:"total_cpu_cores,omitempty"` + AvailableCpuCores float64 `protobuf:"fixed64,9,opt,name=available_cpu_cores,json=availableCpuCores,proto3" json:"available_cpu_cores,omitempty"` + TotalMemoryMb int64 `protobuf:"varint,10,opt,name=total_memory_mb,json=totalMemoryMb,proto3" json:"total_memory_mb,omitempty"` + AvailableMemoryMb int64 `protobuf:"varint,11,opt,name=available_memory_mb,json=availableMemoryMb,proto3" json:"available_memory_mb,omitempty"` + SupportedWorkloadTypes []string `protobuf:"bytes,12,rep,name=supported_workload_types,json=supportedWorkloadTypes,proto3" json:"supported_workload_types,omitempty"` + Labels map[string]string `protobuf:"bytes,13,rep,name=labels,proto3" json:"labels,omitempty" protobuf_key:"bytes,1,opt,name=key" protobuf_val:"bytes,2,opt,name=value"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *NodeView) Reset() { + *x = NodeView{} + mi := &file_control_proto_msgTypes[28] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *NodeView) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*NodeView) ProtoMessage() {} + +func (x *NodeView) ProtoReflect() protoreflect.Message { + mi := &file_control_proto_msgTypes[28] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use NodeView.ProtoReflect.Descriptor instead. +func (*NodeView) Descriptor() ([]byte, []int) { + return file_control_proto_rawDescGZIP(), []int{28} +} + +func (x *NodeView) GetNodeId() string { + if x != nil { + return x.NodeId + } + return "" +} + +func (x *NodeView) GetStatus() string { + if x != nil { + return x.Status + } + return "" +} + +func (x *NodeView) GetStatusReason() string { + if x != nil { + return x.StatusReason + } + return "" +} + +func (x *NodeView) GetStatusUpdatedBy() string { + if x != nil { + return x.StatusUpdatedBy + } + return "" +} + +func (x *NodeView) GetStatusUpdatedAt() *timestamppb.Timestamp { + if x != nil { + return x.StatusUpdatedAt + } + return nil +} + +func (x *NodeView) GetLastHeartbeat() *timestamppb.Timestamp { + if x != nil { + return x.LastHeartbeat + } + return nil +} + +func (x *NodeView) GetGrpcEndpoint() string { + if x != nil { + return x.GrpcEndpoint + } + return "" +} + +func (x *NodeView) GetTotalCpuCores() float64 { + if x != nil { + return x.TotalCpuCores + } + return 0 +} + +func (x *NodeView) GetAvailableCpuCores() float64 { + if x != nil { + return x.AvailableCpuCores + } + return 0 +} + +func (x *NodeView) GetTotalMemoryMb() int64 { + if x != nil { + return x.TotalMemoryMb + } + return 0 +} + +func (x *NodeView) GetAvailableMemoryMb() int64 { + if x != nil { + return x.AvailableMemoryMb + } + return 0 +} + +func (x *NodeView) GetSupportedWorkloadTypes() []string { + if x != nil { + return x.SupportedWorkloadTypes + } + return nil +} + +func (x *NodeView) GetLabels() map[string]string { + if x != nil { + return x.Labels + } + return nil +} + +type ListWorkloadsRequest struct { + state protoimpl.MessageState `protogen:"open.v1"` + NodeId string `protobuf:"bytes,1,opt,name=node_id,json=nodeId,proto3" json:"node_id,omitempty"` // optional filter + Status string `protobuf:"bytes,2,opt,name=status,proto3" json:"status,omitempty"` // optional filter + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *ListWorkloadsRequest) Reset() { + *x = ListWorkloadsRequest{} + mi := &file_control_proto_msgTypes[29] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *ListWorkloadsRequest) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*ListWorkloadsRequest) ProtoMessage() {} + +func (x *ListWorkloadsRequest) ProtoReflect() protoreflect.Message { + mi := &file_control_proto_msgTypes[29] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use ListWorkloadsRequest.ProtoReflect.Descriptor instead. +func (*ListWorkloadsRequest) Descriptor() ([]byte, []int) { + return file_control_proto_rawDescGZIP(), []int{29} +} + +func (x *ListWorkloadsRequest) GetNodeId() string { + if x != nil { + return x.NodeId + } + return "" +} + +func (x *ListWorkloadsRequest) GetStatus() string { + if x != nil { + return x.Status + } + return "" +} + +type GetWorkloadRequest struct { + state protoimpl.MessageState `protogen:"open.v1"` + WorkloadId string `protobuf:"bytes,1,opt,name=workload_id,json=workloadId,proto3" json:"workload_id,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *GetWorkloadRequest) Reset() { + *x = GetWorkloadRequest{} + mi := &file_control_proto_msgTypes[30] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *GetWorkloadRequest) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*GetWorkloadRequest) ProtoMessage() {} + +func (x *GetWorkloadRequest) ProtoReflect() protoreflect.Message { + mi := &file_control_proto_msgTypes[30] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use GetWorkloadRequest.ProtoReflect.Descriptor instead. +func (*GetWorkloadRequest) Descriptor() ([]byte, []int) { + return file_control_proto_rawDescGZIP(), []int{30} +} + +func (x *GetWorkloadRequest) GetWorkloadId() string { + if x != nil { + return x.WorkloadId + } + return "" +} + +type ListWorkloadsResponse struct { + state protoimpl.MessageState `protogen:"open.v1"` + Workloads []*WorkloadView `protobuf:"bytes,1,rep,name=workloads,proto3" json:"workloads,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *ListWorkloadsResponse) Reset() { + *x = ListWorkloadsResponse{} + mi := &file_control_proto_msgTypes[31] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *ListWorkloadsResponse) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*ListWorkloadsResponse) ProtoMessage() {} + +func (x *ListWorkloadsResponse) ProtoReflect() protoreflect.Message { + mi := &file_control_proto_msgTypes[31] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use ListWorkloadsResponse.ProtoReflect.Descriptor instead. +func (*ListWorkloadsResponse) Descriptor() ([]byte, []int) { + return file_control_proto_rawDescGZIP(), []int{31} +} + +func (x *ListWorkloadsResponse) GetWorkloads() []*WorkloadView { + if x != nil { + return x.Workloads + } + return nil +} + +type GetWorkloadResponse struct { + state protoimpl.MessageState `protogen:"open.v1"` + Workload *WorkloadView `protobuf:"bytes,1,opt,name=workload,proto3" json:"workload,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *GetWorkloadResponse) Reset() { + *x = GetWorkloadResponse{} + mi := &file_control_proto_msgTypes[32] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *GetWorkloadResponse) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*GetWorkloadResponse) ProtoMessage() {} + +func (x *GetWorkloadResponse) ProtoReflect() protoreflect.Message { + mi := &file_control_proto_msgTypes[32] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use GetWorkloadResponse.ProtoReflect.Descriptor instead. +func (*GetWorkloadResponse) Descriptor() ([]byte, []int) { + return file_control_proto_rawDescGZIP(), []int{32} +} + +func (x *GetWorkloadResponse) GetWorkload() *WorkloadView { + if x != nil { + return x.Workload + } + return nil +} + +type WorkloadView struct { + state protoimpl.MessageState `protogen:"open.v1"` + WorkloadId string `protobuf:"bytes,1,opt,name=workload_id,json=workloadId,proto3" json:"workload_id,omitempty"` + Type string `protobuf:"bytes,2,opt,name=type,proto3" json:"type,omitempty"` + DesiredState string `protobuf:"bytes,3,opt,name=desired_state,json=desiredState,proto3" json:"desired_state,omitempty"` + Status string `protobuf:"bytes,4,opt,name=status,proto3" json:"status,omitempty"` + AssignedNodeId string `protobuf:"bytes,5,opt,name=assigned_node_id,json=assignedNodeId,proto3" json:"assigned_node_id,omitempty"` + RevisionId string `protobuf:"bytes,6,opt,name=revision_id,json=revisionId,proto3" json:"revision_id,omitempty"` + RetryAttempts int32 `protobuf:"varint,7,opt,name=retry_attempts,json=retryAttempts,proto3" json:"retry_attempts,omitempty"` + RetryMaxAttempts int32 `protobuf:"varint,8,opt,name=retry_max_attempts,json=retryMaxAttempts,proto3" json:"retry_max_attempts,omitempty"` + RetryNextAt *timestamppb.Timestamp `protobuf:"bytes,9,opt,name=retry_next_at,json=retryNextAt,proto3" json:"retry_next_at,omitempty"` + FailureReason string `protobuf:"bytes,10,opt,name=failure_reason,json=failureReason,proto3" json:"failure_reason,omitempty"` + LastUpdated *timestamppb.Timestamp `protobuf:"bytes,11,opt,name=last_updated,json=lastUpdated,proto3" json:"last_updated,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *WorkloadView) Reset() { + *x = WorkloadView{} + mi := &file_control_proto_msgTypes[33] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *WorkloadView) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*WorkloadView) ProtoMessage() {} + +func (x *WorkloadView) ProtoReflect() protoreflect.Message { + mi := &file_control_proto_msgTypes[33] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use WorkloadView.ProtoReflect.Descriptor instead. +func (*WorkloadView) Descriptor() ([]byte, []int) { + return file_control_proto_rawDescGZIP(), []int{33} +} + +func (x *WorkloadView) GetWorkloadId() string { + if x != nil { + return x.WorkloadId + } + return "" +} + +func (x *WorkloadView) GetType() string { + if x != nil { + return x.Type + } + return "" +} + +func (x *WorkloadView) GetDesiredState() string { + if x != nil { + return x.DesiredState + } + return "" +} + +func (x *WorkloadView) GetStatus() string { + if x != nil { + return x.Status + } + return "" +} + +func (x *WorkloadView) GetAssignedNodeId() string { + if x != nil { + return x.AssignedNodeId + } + return "" +} + +func (x *WorkloadView) GetRevisionId() string { + if x != nil { + return x.RevisionId + } + return "" +} + +func (x *WorkloadView) GetRetryAttempts() int32 { + if x != nil { + return x.RetryAttempts + } + return 0 +} + +func (x *WorkloadView) GetRetryMaxAttempts() int32 { + if x != nil { + return x.RetryMaxAttempts + } + return 0 +} + +func (x *WorkloadView) GetRetryNextAt() *timestamppb.Timestamp { + if x != nil { + return x.RetryNextAt + } + return nil +} + +func (x *WorkloadView) GetFailureReason() string { + if x != nil { + return x.FailureReason + } + return "" +} + +func (x *WorkloadView) GetLastUpdated() *timestamppb.Timestamp { + if x != nil { + return x.LastUpdated + } + return nil +} + +type GetClusterSummaryRequest struct { + state protoimpl.MessageState `protogen:"open.v1"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *GetClusterSummaryRequest) Reset() { + *x = GetClusterSummaryRequest{} + mi := &file_control_proto_msgTypes[34] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *GetClusterSummaryRequest) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*GetClusterSummaryRequest) ProtoMessage() {} + +func (x *GetClusterSummaryRequest) ProtoReflect() protoreflect.Message { + mi := &file_control_proto_msgTypes[34] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use GetClusterSummaryRequest.ProtoReflect.Descriptor instead. +func (*GetClusterSummaryRequest) Descriptor() ([]byte, []int) { + return file_control_proto_rawDescGZIP(), []int{34} +} + +type GetClusterSummaryResponse struct { + state protoimpl.MessageState `protogen:"open.v1"` + TotalNodes int32 `protobuf:"varint,1,opt,name=total_nodes,json=totalNodes,proto3" json:"total_nodes,omitempty"` + ReadyNodes int32 `protobuf:"varint,2,opt,name=ready_nodes,json=readyNodes,proto3" json:"ready_nodes,omitempty"` + NotReadyNodes int32 `protobuf:"varint,3,opt,name=not_ready_nodes,json=notReadyNodes,proto3" json:"not_ready_nodes,omitempty"` + TotalWorkloads int32 `protobuf:"varint,4,opt,name=total_workloads,json=totalWorkloads,proto3" json:"total_workloads,omitempty"` + RunningWorkloads int32 `protobuf:"varint,5,opt,name=running_workloads,json=runningWorkloads,proto3" json:"running_workloads,omitempty"` + PendingWorkloads int32 `protobuf:"varint,6,opt,name=pending_workloads,json=pendingWorkloads,proto3" json:"pending_workloads,omitempty"` + FailedWorkloads int32 `protobuf:"varint,7,opt,name=failed_workloads,json=failedWorkloads,proto3" json:"failed_workloads,omitempty"` + DeletedWorkloads int32 `protobuf:"varint,8,opt,name=deleted_workloads,json=deletedWorkloads,proto3" json:"deleted_workloads,omitempty"` + GeneratedAt *timestamppb.Timestamp `protobuf:"bytes,9,opt,name=generated_at,json=generatedAt,proto3" json:"generated_at,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *GetClusterSummaryResponse) Reset() { + *x = GetClusterSummaryResponse{} + mi := &file_control_proto_msgTypes[35] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *GetClusterSummaryResponse) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*GetClusterSummaryResponse) ProtoMessage() {} + +func (x *GetClusterSummaryResponse) ProtoReflect() protoreflect.Message { + mi := &file_control_proto_msgTypes[35] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use GetClusterSummaryResponse.ProtoReflect.Descriptor instead. +func (*GetClusterSummaryResponse) Descriptor() ([]byte, []int) { + return file_control_proto_rawDescGZIP(), []int{35} +} + +func (x *GetClusterSummaryResponse) GetTotalNodes() int32 { + if x != nil { + return x.TotalNodes + } + return 0 +} + +func (x *GetClusterSummaryResponse) GetReadyNodes() int32 { + if x != nil { + return x.ReadyNodes + } + return 0 +} + +func (x *GetClusterSummaryResponse) GetNotReadyNodes() int32 { + if x != nil { + return x.NotReadyNodes + } + return 0 +} + +func (x *GetClusterSummaryResponse) GetTotalWorkloads() int32 { + if x != nil { + return x.TotalWorkloads + } + return 0 +} + +func (x *GetClusterSummaryResponse) GetRunningWorkloads() int32 { + if x != nil { + return x.RunningWorkloads + } + return 0 +} + +func (x *GetClusterSummaryResponse) GetPendingWorkloads() int32 { + if x != nil { + return x.PendingWorkloads + } + return 0 +} + +func (x *GetClusterSummaryResponse) GetFailedWorkloads() int32 { + if x != nil { + return x.FailedWorkloads + } + return 0 +} + +func (x *GetClusterSummaryResponse) GetDeletedWorkloads() int32 { + if x != nil { + return x.DeletedWorkloads + } + return 0 +} + +func (x *GetClusterSummaryResponse) GetGeneratedAt() *timestamppb.Timestamp { + if x != nil { + return x.GeneratedAt + } + return nil +} + +type ControlMessage struct { + state protoimpl.MessageState `protogen:"open.v1"` + // Types that are valid to be assigned to Message: + // + // *ControlMessage_Register + // *ControlMessage_Heartbeat + // *ControlMessage_Apply + // *ControlMessage_Delete + Message isControlMessage_Message `protobuf_oneof:"message"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *ControlMessage) Reset() { + *x = ControlMessage{} + mi := &file_control_proto_msgTypes[36] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *ControlMessage) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*ControlMessage) ProtoMessage() {} + +func (x *ControlMessage) ProtoReflect() protoreflect.Message { + mi := &file_control_proto_msgTypes[36] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use ControlMessage.ProtoReflect.Descriptor instead. +func (*ControlMessage) Descriptor() ([]byte, []int) { + return file_control_proto_rawDescGZIP(), []int{36} +} + +func (x *ControlMessage) GetMessage() isControlMessage_Message { + if x != nil { + return x.Message + } + return nil +} + +func (x *ControlMessage) GetRegister() *RegisterNodeRequest { + if x != nil { + if x, ok := x.Message.(*ControlMessage_Register); ok { + return x.Register + } + } + return nil +} + +func (x *ControlMessage) GetHeartbeat() *HeartbeatRequest { + if x != nil { + if x, ok := x.Message.(*ControlMessage_Heartbeat); ok { + return x.Heartbeat + } + } + return nil +} + +func (x *ControlMessage) GetApply() *ApplyWorkloadRequest { + if x != nil { + if x, ok := x.Message.(*ControlMessage_Apply); ok { + return x.Apply + } + } + return nil +} + +func (x *ControlMessage) GetDelete() *DeleteWorkloadRequest { + if x != nil { + if x, ok := x.Message.(*ControlMessage_Delete); ok { + return x.Delete + } + } + return nil +} + +type isControlMessage_Message interface { + isControlMessage_Message() +} + +type ControlMessage_Register struct { + Register *RegisterNodeRequest `protobuf:"bytes,1,opt,name=register,proto3,oneof"` +} + +type ControlMessage_Heartbeat struct { + Heartbeat *HeartbeatRequest `protobuf:"bytes,2,opt,name=heartbeat,proto3,oneof"` +} + +type ControlMessage_Apply struct { + Apply *ApplyWorkloadRequest `protobuf:"bytes,3,opt,name=apply,proto3,oneof"` +} + +type ControlMessage_Delete struct { + Delete *DeleteWorkloadRequest `protobuf:"bytes,4,opt,name=delete,proto3,oneof"` +} + +func (*ControlMessage_Register) isControlMessage_Message() {} + +func (*ControlMessage_Heartbeat) isControlMessage_Message() {} + +func (*ControlMessage_Apply) isControlMessage_Message() {} + +func (*ControlMessage_Delete) isControlMessage_Message() {} + +var File_control_proto protoreflect.FileDescriptor + +const file_control_proto_rawDesc = "" + + "\n" + + "\rcontrol.proto\x12\x11persys.control.v1\x1a\x1fgoogle/protobuf/timestamp.proto\"\xa1\x03\n" + + "\x13RegisterNodeRequest\x12\x17\n" + + "\anode_id\x18\x01 \x01(\tR\x06nodeId\x12G\n" + + "\fcapabilities\x18\x02 \x01(\v2#.persys.control.v1.NodeCapabilitiesR\fcapabilities\x12J\n" + + "\x06labels\x18\x03 \x03(\v22.persys.control.v1.RegisterNodeRequest.LabelsEntryR\x06labels\x12#\n" + + "\ragent_version\x18\x04 \x01(\tR\fagentVersion\x12#\n" + + "\rgrpc_endpoint\x18\x05 \x01(\tR\fgrpcEndpoint\x12\x1d\n" + + "\n" + + "cluster_id\x18\x06 \x01(\tR\tclusterId\x128\n" + + "\ttimestamp\x18\a \x01(\v2\x1a.google.protobuf.TimestampR\ttimestamp\x1a9\n" + + "\vLabelsEntry\x12\x10\n" + + "\x03key\x18\x01 \x01(\tR\x03key\x12\x14\n" + + "\x05value\x18\x02 \x01(\tR\x05value:\x028\x01\"\xeb\x01\n" + + "\x10NodeCapabilities\x120\n" + + "\x14cpu_total_millicores\x18\x01 \x01(\x03R\x12cpuTotalMillicores\x12&\n" + + "\x0fmemory_total_mb\x18\x02 \x01(\x03R\rmemoryTotalMb\x12C\n" + + "\rstorage_pools\x18\x03 \x03(\v2\x1e.persys.control.v1.StoragePoolR\fstoragePools\x128\n" + + "\x18supported_workload_types\x18\x04 \x03(\tR\x16supportedWorkloadTypes\"P\n" + + "\vStoragePool\x12\x12\n" + + "\x04name\x18\x01 \x01(\tR\x04name\x12\x12\n" + + "\x04type\x18\x02 \x01(\tR\x04type\x12\x19\n" + + "\btotal_gb\x18\x03 \x01(\x03R\atotalGb\"\xce\x01\n" + + "\x14RegisterNodeResponse\x12\x1a\n" + + "\baccepted\x18\x01 \x01(\bR\baccepted\x12\x16\n" + + "\x06reason\x18\x02 \x01(\tR\x06reason\x12<\n" + + "\x1aheartbeat_interval_seconds\x18\x03 \x01(\x05R\x18heartbeatIntervalSeconds\x12D\n" + + "\x10lease_expires_at\x18\x04 \x01(\v2\x1a.google.protobuf.TimestampR\x0eleaseExpiresAt\"\xe9\x01\n" + + "\x10HeartbeatRequest\x12\x17\n" + + "\anode_id\x18\x01 \x01(\tR\x06nodeId\x122\n" + + "\x05usage\x18\x02 \x01(\v2\x1c.persys.control.v1.NodeUsageR\x05usage\x12N\n" + + "\x11workload_statuses\x18\x03 \x03(\v2!.persys.control.v1.WorkloadStatusR\x10workloadStatuses\x128\n" + + "\ttimestamp\x18\x04 \x01(\v2\x1a.google.protobuf.TimestampR\ttimestamp\"\x99\x02\n" + + "\tNodeUsage\x128\n" + + "\x18cpu_allocated_millicores\x18\x01 \x01(\x03R\x16cpuAllocatedMillicores\x12.\n" + + "\x13cpu_used_millicores\x18\x02 \x01(\x03R\x11cpuUsedMillicores\x12.\n" + + "\x13memory_allocated_mb\x18\x03 \x01(\x03R\x11memoryAllocatedMb\x12$\n" + + "\x0ememory_used_mb\x18\x04 \x01(\x03R\fmemoryUsedMb\x12*\n" + + "\x11disk_allocated_gb\x18\x05 \x01(\x03R\x0fdiskAllocatedGb\x12 \n" + + "\fdisk_used_gb\x18\x06 \x01(\x03R\n" + + "diskUsedGb\"\x9c\x01\n" + + "\x11HeartbeatResponse\x12\"\n" + + "\facknowledged\x18\x01 \x01(\bR\facknowledged\x12\x1d\n" + + "\n" + + "drain_node\x18\x02 \x01(\bR\tdrainNode\x12D\n" + + "\x10lease_expires_at\x18\x03 \x01(\v2\x1a.google.protobuf.TimestampR\x0eleaseExpiresAt\"\xb2\x01\n" + + "\x14ApplyWorkloadRequest\x12\x1f\n" + + "\vworkload_id\x18\x01 \x01(\tR\n" + + "workloadId\x123\n" + + "\x04spec\x18\x02 \x01(\v2\x1f.persys.control.v1.WorkloadSpecR\x04spec\x12\x1f\n" + + "\vrevision_id\x18\n" + + " \x01(\tR\n" + + "revisionId\x12#\n" + + "\rdesired_state\x18\v \x01(\tR\fdesiredState\"\x9f\x01\n" + + "\x15ApplyWorkloadResponse\x12\x18\n" + + "\asuccess\x18\x01 \x01(\bR\asuccess\x12G\n" + + "\x0efailure_reason\x18\x02 \x01(\x0e2 .persys.control.v1.FailureReasonR\rfailureReason\x12#\n" + + "\rerror_message\x18\x03 \x01(\tR\ferrorMessage\"8\n" + + "\x15DeleteWorkloadRequest\x12\x1f\n" + + "\vworkload_id\x18\x01 \x01(\tR\n" + + "workloadId\"W\n" + + "\x16DeleteWorkloadResponse\x12\x18\n" + + "\asuccess\x18\x01 \x01(\bR\asuccess\x12#\n" + + "\rerror_message\x18\x02 \x01(\tR\ferrorMessage\"\xa8\x03\n" + + "\fWorkloadSpec\x12\x12\n" + + "\x04type\x18\x01 \x01(\tR\x04type\x12E\n" + + "\tresources\x18\x02 \x01(\v2'.persys.control.v1.ResourceRequirementsR\tresources\x12@\n" + + "\tcontainer\x18\n" + + " \x01(\v2 .persys.control.v1.ContainerSpecH\x00R\tcontainer\x12:\n" + + "\acompose\x18\v \x01(\v2\x1e.persys.control.v1.ComposeSpecH\x00R\acompose\x12+\n" + + "\x02vm\x18\f \x01(\v2\x19.persys.control.v1.VMSpecH\x00R\x02vm\x12I\n" + + "\bmetadata\x18\x14 \x03(\v2-.persys.control.v1.WorkloadSpec.MetadataEntryR\bmetadata\x1a;\n" + + "\rMetadataEntry\x12\x10\n" + + "\x03key\x18\x01 \x01(\tR\x03key\x12\x14\n" + + "\x05value\x18\x02 \x01(\tR\x05value:\x028\x01B\n" + + "\n" + + "\bworkload\"s\n" + + "\x14ResourceRequirements\x12%\n" + + "\x0ecpu_millicores\x18\x01 \x01(\x03R\rcpuMillicores\x12\x1b\n" + + "\tmemory_mb\x18\x02 \x01(\x03R\bmemoryMb\x12\x17\n" + + "\adisk_gb\x18\x03 \x01(\x03R\x06diskGb\"\xe4\x02\n" + + "\rContainerSpec\x12\x14\n" + + "\x05image\x18\x01 \x01(\tR\x05image\x12\x18\n" + + "\acommand\x18\x02 \x03(\tR\acommand\x12;\n" + + "\x03env\x18\x03 \x03(\v2).persys.control.v1.ContainerSpec.EnvEntryR\x03env\x128\n" + + "\avolumes\x18\x04 \x03(\v2\x1e.persys.control.v1.VolumeMountR\avolumes\x12-\n" + + "\x05ports\x18\x05 \x03(\v2\x17.persys.control.v1.PortR\x05ports\x12%\n" + + "\x0erestart_policy\x18\x06 \x01(\tR\rrestartPolicy\x12\x1e\n" + + "\n" + + "privileged\x18\a \x01(\bR\n" + + "privileged\x1a6\n" + + "\bEnvEntry\x12\x10\n" + + "\x03key\x18\x01 \x01(\tR\x03key\x12\x14\n" + + "\x05value\x18\x02 \x01(\tR\x05value:\x028\x01\"n\n" + + "\vVolumeMount\x12\x1b\n" + + "\thost_path\x18\x01 \x01(\tR\bhostPath\x12%\n" + + "\x0econtainer_path\x18\x02 \x01(\tR\rcontainerPath\x12\x1b\n" + + "\tread_only\x18\x03 \x01(\bR\breadOnly\"f\n" + + "\x04Port\x12\x1b\n" + + "\thost_port\x18\x01 \x01(\x05R\bhostPort\x12%\n" + + "\x0econtainer_port\x18\x02 \x01(\x05R\rcontainerPort\x12\x1a\n" + + "\bprotocol\x18\x03 \x01(\tR\bprotocol\"\xf6\x01\n" + + "\vComposeSpec\x12\x1f\n" + + "\vsource_type\x18\x01 \x01(\tR\n" + + "sourceType\x12\x19\n" + + "\bgit_repo\x18\x02 \x01(\tR\agitRepo\x12\x17\n" + + "\agit_ref\x18\x03 \x01(\tR\x06gitRef\x12\x1f\n" + + "\vinline_yaml\x18\x04 \x01(\tR\n" + + "inlineYaml\x129\n" + + "\x03env\x18\x05 \x03(\v2'.persys.control.v1.ComposeSpec.EnvEntryR\x03env\x1a6\n" + + "\bEnvEntry\x12\x10\n" + + "\x03key\x18\x01 \x01(\tR\x03key\x12\x14\n" + + "\x05value\x18\x02 \x01(\tR\x05value:\x028\x01\"\x8c\x02\n" + + "\x06VMSpec\x12\x14\n" + + "\x05vcpus\x18\x01 \x01(\x05R\x05vcpus\x12\x1b\n" + + "\tmemory_mb\x18\x02 \x01(\x03R\bmemoryMb\x123\n" + + "\x05disks\x18\x03 \x03(\v2\x1d.persys.control.v1.DiskConfigR\x05disks\x12<\n" + + "\bnetworks\x18\x04 \x03(\v2 .persys.control.v1.NetworkConfigR\bnetworks\x12A\n" + + "\n" + + "cloud_init\x18\x05 \x01(\v2\".persys.control.v1.CloudInitConfigR\tcloudInit\x12\x19\n" + + "\bos_image\x18\x06 \x01(\tR\aosImage\"c\n" + + "\n" + + "DiskConfig\x12\x1b\n" + + "\tpool_name\x18\x01 \x01(\tR\bpoolName\x12\x17\n" + + "\asize_gb\x18\x02 \x01(\x03R\x06sizeGb\x12\x1f\n" + + "\vmount_point\x18\x03 \x01(\tR\n" + + "mountPoint\"X\n" + + "\rNetworkConfig\x12\x16\n" + + "\x06bridge\x18\x01 \x01(\tR\x06bridge\x12\x12\n" + + "\x04dhcp\x18\x02 \x01(\bR\x04dhcp\x12\x1b\n" + + "\tstatic_ip\x18\x03 \x01(\tR\bstaticIp\"r\n" + + "\x0fCloudInitConfig\x12\x1b\n" + + "\tuser_data\x18\x01 \x01(\tR\buserData\x12\x1b\n" + + "\tmeta_data\x18\x02 \x01(\tR\bmetaData\x12%\n" + + "\x0enetwork_config\x18\x03 \x01(\tR\rnetworkConfig\"\xef\x01\n" + + "\x0eWorkloadStatus\x12\x1f\n" + + "\vworkload_id\x18\x01 \x01(\tR\n" + + "workloadId\x12\x14\n" + + "\x05state\x18\x02 \x01(\tR\x05state\x12G\n" + + "\x0efailure_reason\x18\x03 \x01(\x0e2 .persys.control.v1.FailureReasonR\rfailureReason\x12\x18\n" + + "\amessage\x18\x04 \x01(\tR\amessage\x12C\n" + + "\x0flast_transition\x18\x05 \x01(\v2\x1a.google.protobuf.TimestampR\x0elastTransition\"7\n" + + "\x14RetryWorkloadRequest\x12\x1f\n" + + "\vworkload_id\x18\x01 \x01(\tR\n" + + "workloadId\"3\n" + + "\x15RetryWorkloadResponse\x12\x1a\n" + + "\baccepted\x18\x01 \x01(\bR\baccepted\"*\n" + + "\x10ListNodesRequest\x12\x16\n" + + "\x06status\x18\x01 \x01(\tR\x06status\")\n" + + "\x0eGetNodeRequest\x12\x17\n" + + "\anode_id\x18\x01 \x01(\tR\x06nodeId\"F\n" + + "\x11ListNodesResponse\x121\n" + + "\x05nodes\x18\x01 \x03(\v2\x1b.persys.control.v1.NodeViewR\x05nodes\"B\n" + + "\x0fGetNodeResponse\x12/\n" + + "\x04node\x18\x01 \x01(\v2\x1b.persys.control.v1.NodeViewR\x04node\"\xa2\x05\n" + + "\bNodeView\x12\x17\n" + + "\anode_id\x18\x01 \x01(\tR\x06nodeId\x12\x16\n" + + "\x06status\x18\x02 \x01(\tR\x06status\x12#\n" + + "\rstatus_reason\x18\x03 \x01(\tR\fstatusReason\x12*\n" + + "\x11status_updated_by\x18\x04 \x01(\tR\x0fstatusUpdatedBy\x12F\n" + + "\x11status_updated_at\x18\x05 \x01(\v2\x1a.google.protobuf.TimestampR\x0fstatusUpdatedAt\x12A\n" + + "\x0elast_heartbeat\x18\x06 \x01(\v2\x1a.google.protobuf.TimestampR\rlastHeartbeat\x12#\n" + + "\rgrpc_endpoint\x18\a \x01(\tR\fgrpcEndpoint\x12&\n" + + "\x0ftotal_cpu_cores\x18\b \x01(\x01R\rtotalCpuCores\x12.\n" + + "\x13available_cpu_cores\x18\t \x01(\x01R\x11availableCpuCores\x12&\n" + + "\x0ftotal_memory_mb\x18\n" + + " \x01(\x03R\rtotalMemoryMb\x12.\n" + + "\x13available_memory_mb\x18\v \x01(\x03R\x11availableMemoryMb\x128\n" + + "\x18supported_workload_types\x18\f \x03(\tR\x16supportedWorkloadTypes\x12?\n" + + "\x06labels\x18\r \x03(\v2'.persys.control.v1.NodeView.LabelsEntryR\x06labels\x1a9\n" + + "\vLabelsEntry\x12\x10\n" + + "\x03key\x18\x01 \x01(\tR\x03key\x12\x14\n" + + "\x05value\x18\x02 \x01(\tR\x05value:\x028\x01\"G\n" + + "\x14ListWorkloadsRequest\x12\x17\n" + + "\anode_id\x18\x01 \x01(\tR\x06nodeId\x12\x16\n" + + "\x06status\x18\x02 \x01(\tR\x06status\"5\n" + + "\x12GetWorkloadRequest\x12\x1f\n" + + "\vworkload_id\x18\x01 \x01(\tR\n" + + "workloadId\"V\n" + + "\x15ListWorkloadsResponse\x12=\n" + + "\tworkloads\x18\x01 \x03(\v2\x1f.persys.control.v1.WorkloadViewR\tworkloads\"R\n" + + "\x13GetWorkloadResponse\x12;\n" + + "\bworkload\x18\x01 \x01(\v2\x1f.persys.control.v1.WorkloadViewR\bworkload\"\xc6\x03\n" + + "\fWorkloadView\x12\x1f\n" + + "\vworkload_id\x18\x01 \x01(\tR\n" + + "workloadId\x12\x12\n" + + "\x04type\x18\x02 \x01(\tR\x04type\x12#\n" + + "\rdesired_state\x18\x03 \x01(\tR\fdesiredState\x12\x16\n" + + "\x06status\x18\x04 \x01(\tR\x06status\x12(\n" + + "\x10assigned_node_id\x18\x05 \x01(\tR\x0eassignedNodeId\x12\x1f\n" + + "\vrevision_id\x18\x06 \x01(\tR\n" + + "revisionId\x12%\n" + + "\x0eretry_attempts\x18\a \x01(\x05R\rretryAttempts\x12,\n" + + "\x12retry_max_attempts\x18\b \x01(\x05R\x10retryMaxAttempts\x12>\n" + + "\rretry_next_at\x18\t \x01(\v2\x1a.google.protobuf.TimestampR\vretryNextAt\x12%\n" + + "\x0efailure_reason\x18\n" + + " \x01(\tR\rfailureReason\x12=\n" + + "\flast_updated\x18\v \x01(\v2\x1a.google.protobuf.TimestampR\vlastUpdated\"\x1a\n" + + "\x18GetClusterSummaryRequest\"\x9f\x03\n" + + "\x19GetClusterSummaryResponse\x12\x1f\n" + + "\vtotal_nodes\x18\x01 \x01(\x05R\n" + + "totalNodes\x12\x1f\n" + + "\vready_nodes\x18\x02 \x01(\x05R\n" + + "readyNodes\x12&\n" + + "\x0fnot_ready_nodes\x18\x03 \x01(\x05R\rnotReadyNodes\x12'\n" + + "\x0ftotal_workloads\x18\x04 \x01(\x05R\x0etotalWorkloads\x12+\n" + + "\x11running_workloads\x18\x05 \x01(\x05R\x10runningWorkloads\x12+\n" + + "\x11pending_workloads\x18\x06 \x01(\x05R\x10pendingWorkloads\x12)\n" + + "\x10failed_workloads\x18\a \x01(\x05R\x0ffailedWorkloads\x12+\n" + + "\x11deleted_workloads\x18\b \x01(\x05R\x10deletedWorkloads\x12=\n" + + "\fgenerated_at\x18\t \x01(\v2\x1a.google.protobuf.TimestampR\vgeneratedAt\"\xab\x02\n" + + "\x0eControlMessage\x12D\n" + + "\bregister\x18\x01 \x01(\v2&.persys.control.v1.RegisterNodeRequestH\x00R\bregister\x12C\n" + + "\theartbeat\x18\x02 \x01(\v2#.persys.control.v1.HeartbeatRequestH\x00R\theartbeat\x12?\n" + + "\x05apply\x18\x03 \x01(\v2'.persys.control.v1.ApplyWorkloadRequestH\x00R\x05apply\x12B\n" + + "\x06delete\x18\x04 \x01(\v2(.persys.control.v1.DeleteWorkloadRequestH\x00R\x06deleteB\t\n" + + "\amessage*\xd6\x01\n" + + "\rFailureReason\x12\x1e\n" + + "\x1aFAILURE_REASON_UNSPECIFIED\x10\x00\x12\x15\n" + + "\x11IMAGE_PULL_FAILED\x10\x01\x12\x13\n" + + "\x0fIMAGE_NOT_FOUND\x10\x02\x12\x1a\n" + + "\x16INSUFFICIENT_RESOURCES\x10\x03\x12\x10\n" + + "\fINVALID_SPEC\x10\x04\x12\x11\n" + + "\rRUNTIME_ERROR\x10\x05\x12\x11\n" + + "\rNETWORK_ERROR\x10\x06\x12\x11\n" + + "\rSTORAGE_ERROR\x10\a\x12\x12\n" + + "\x0eVM_BOOT_FAILED\x10\b2\xad\b\n" + + "\fAgentControl\x12_\n" + + "\fRegisterNode\x12&.persys.control.v1.RegisterNodeRequest\x1a'.persys.control.v1.RegisterNodeResponse\x12V\n" + + "\tHeartbeat\x12#.persys.control.v1.HeartbeatRequest\x1a$.persys.control.v1.HeartbeatResponse\x12b\n" + + "\rApplyWorkload\x12'.persys.control.v1.ApplyWorkloadRequest\x1a(.persys.control.v1.ApplyWorkloadResponse\x12e\n" + + "\x0eDeleteWorkload\x12(.persys.control.v1.DeleteWorkloadRequest\x1a).persys.control.v1.DeleteWorkloadResponse\x12b\n" + + "\rRetryWorkload\x12'.persys.control.v1.RetryWorkloadRequest\x1a(.persys.control.v1.RetryWorkloadResponse\x12V\n" + + "\tListNodes\x12#.persys.control.v1.ListNodesRequest\x1a$.persys.control.v1.ListNodesResponse\x12P\n" + + "\aGetNode\x12!.persys.control.v1.GetNodeRequest\x1a\".persys.control.v1.GetNodeResponse\x12b\n" + + "\rListWorkloads\x12'.persys.control.v1.ListWorkloadsRequest\x1a(.persys.control.v1.ListWorkloadsResponse\x12\\\n" + + "\vGetWorkload\x12%.persys.control.v1.GetWorkloadRequest\x1a&.persys.control.v1.GetWorkloadResponse\x12n\n" + + "\x11GetClusterSummary\x12+.persys.control.v1.GetClusterSummaryRequest\x1a,.persys.control.v1.GetClusterSummaryResponse\x12Y\n" + + "\rControlStream\x12!.persys.control.v1.ControlMessage\x1a!.persys.control.v1.ControlMessage(\x010\x01B7Z5github.com/persys-dev/persys/api/control/v1;controlv1b\x06proto3" + +var ( + file_control_proto_rawDescOnce sync.Once + file_control_proto_rawDescData []byte +) + +func file_control_proto_rawDescGZIP() []byte { + file_control_proto_rawDescOnce.Do(func() { + file_control_proto_rawDescData = protoimpl.X.CompressGZIP(unsafe.Slice(unsafe.StringData(file_control_proto_rawDesc), len(file_control_proto_rawDesc))) + }) + return file_control_proto_rawDescData +} + +var file_control_proto_enumTypes = make([]protoimpl.EnumInfo, 1) +var file_control_proto_msgTypes = make([]protoimpl.MessageInfo, 42) +var file_control_proto_goTypes = []any{ + (FailureReason)(0), // 0: persys.control.v1.FailureReason + (*RegisterNodeRequest)(nil), // 1: persys.control.v1.RegisterNodeRequest + (*NodeCapabilities)(nil), // 2: persys.control.v1.NodeCapabilities + (*StoragePool)(nil), // 3: persys.control.v1.StoragePool + (*RegisterNodeResponse)(nil), // 4: persys.control.v1.RegisterNodeResponse + (*HeartbeatRequest)(nil), // 5: persys.control.v1.HeartbeatRequest + (*NodeUsage)(nil), // 6: persys.control.v1.NodeUsage + (*HeartbeatResponse)(nil), // 7: persys.control.v1.HeartbeatResponse + (*ApplyWorkloadRequest)(nil), // 8: persys.control.v1.ApplyWorkloadRequest + (*ApplyWorkloadResponse)(nil), // 9: persys.control.v1.ApplyWorkloadResponse + (*DeleteWorkloadRequest)(nil), // 10: persys.control.v1.DeleteWorkloadRequest + (*DeleteWorkloadResponse)(nil), // 11: persys.control.v1.DeleteWorkloadResponse + (*WorkloadSpec)(nil), // 12: persys.control.v1.WorkloadSpec + (*ResourceRequirements)(nil), // 13: persys.control.v1.ResourceRequirements + (*ContainerSpec)(nil), // 14: persys.control.v1.ContainerSpec + (*VolumeMount)(nil), // 15: persys.control.v1.VolumeMount + (*Port)(nil), // 16: persys.control.v1.Port + (*ComposeSpec)(nil), // 17: persys.control.v1.ComposeSpec + (*VMSpec)(nil), // 18: persys.control.v1.VMSpec + (*DiskConfig)(nil), // 19: persys.control.v1.DiskConfig + (*NetworkConfig)(nil), // 20: persys.control.v1.NetworkConfig + (*CloudInitConfig)(nil), // 21: persys.control.v1.CloudInitConfig + (*WorkloadStatus)(nil), // 22: persys.control.v1.WorkloadStatus + (*RetryWorkloadRequest)(nil), // 23: persys.control.v1.RetryWorkloadRequest + (*RetryWorkloadResponse)(nil), // 24: persys.control.v1.RetryWorkloadResponse + (*ListNodesRequest)(nil), // 25: persys.control.v1.ListNodesRequest + (*GetNodeRequest)(nil), // 26: persys.control.v1.GetNodeRequest + (*ListNodesResponse)(nil), // 27: persys.control.v1.ListNodesResponse + (*GetNodeResponse)(nil), // 28: persys.control.v1.GetNodeResponse + (*NodeView)(nil), // 29: persys.control.v1.NodeView + (*ListWorkloadsRequest)(nil), // 30: persys.control.v1.ListWorkloadsRequest + (*GetWorkloadRequest)(nil), // 31: persys.control.v1.GetWorkloadRequest + (*ListWorkloadsResponse)(nil), // 32: persys.control.v1.ListWorkloadsResponse + (*GetWorkloadResponse)(nil), // 33: persys.control.v1.GetWorkloadResponse + (*WorkloadView)(nil), // 34: persys.control.v1.WorkloadView + (*GetClusterSummaryRequest)(nil), // 35: persys.control.v1.GetClusterSummaryRequest + (*GetClusterSummaryResponse)(nil), // 36: persys.control.v1.GetClusterSummaryResponse + (*ControlMessage)(nil), // 37: persys.control.v1.ControlMessage + nil, // 38: persys.control.v1.RegisterNodeRequest.LabelsEntry + nil, // 39: persys.control.v1.WorkloadSpec.MetadataEntry + nil, // 40: persys.control.v1.ContainerSpec.EnvEntry + nil, // 41: persys.control.v1.ComposeSpec.EnvEntry + nil, // 42: persys.control.v1.NodeView.LabelsEntry + (*timestamppb.Timestamp)(nil), // 43: google.protobuf.Timestamp +} +var file_control_proto_depIdxs = []int32{ + 2, // 0: persys.control.v1.RegisterNodeRequest.capabilities:type_name -> persys.control.v1.NodeCapabilities + 38, // 1: persys.control.v1.RegisterNodeRequest.labels:type_name -> persys.control.v1.RegisterNodeRequest.LabelsEntry + 43, // 2: persys.control.v1.RegisterNodeRequest.timestamp:type_name -> google.protobuf.Timestamp + 3, // 3: persys.control.v1.NodeCapabilities.storage_pools:type_name -> persys.control.v1.StoragePool + 43, // 4: persys.control.v1.RegisterNodeResponse.lease_expires_at:type_name -> google.protobuf.Timestamp + 6, // 5: persys.control.v1.HeartbeatRequest.usage:type_name -> persys.control.v1.NodeUsage + 22, // 6: persys.control.v1.HeartbeatRequest.workload_statuses:type_name -> persys.control.v1.WorkloadStatus + 43, // 7: persys.control.v1.HeartbeatRequest.timestamp:type_name -> google.protobuf.Timestamp + 43, // 8: persys.control.v1.HeartbeatResponse.lease_expires_at:type_name -> google.protobuf.Timestamp + 12, // 9: persys.control.v1.ApplyWorkloadRequest.spec:type_name -> persys.control.v1.WorkloadSpec + 0, // 10: persys.control.v1.ApplyWorkloadResponse.failure_reason:type_name -> persys.control.v1.FailureReason + 13, // 11: persys.control.v1.WorkloadSpec.resources:type_name -> persys.control.v1.ResourceRequirements + 14, // 12: persys.control.v1.WorkloadSpec.container:type_name -> persys.control.v1.ContainerSpec + 17, // 13: persys.control.v1.WorkloadSpec.compose:type_name -> persys.control.v1.ComposeSpec + 18, // 14: persys.control.v1.WorkloadSpec.vm:type_name -> persys.control.v1.VMSpec + 39, // 15: persys.control.v1.WorkloadSpec.metadata:type_name -> persys.control.v1.WorkloadSpec.MetadataEntry + 40, // 16: persys.control.v1.ContainerSpec.env:type_name -> persys.control.v1.ContainerSpec.EnvEntry + 15, // 17: persys.control.v1.ContainerSpec.volumes:type_name -> persys.control.v1.VolumeMount + 16, // 18: persys.control.v1.ContainerSpec.ports:type_name -> persys.control.v1.Port + 41, // 19: persys.control.v1.ComposeSpec.env:type_name -> persys.control.v1.ComposeSpec.EnvEntry + 19, // 20: persys.control.v1.VMSpec.disks:type_name -> persys.control.v1.DiskConfig + 20, // 21: persys.control.v1.VMSpec.networks:type_name -> persys.control.v1.NetworkConfig + 21, // 22: persys.control.v1.VMSpec.cloud_init:type_name -> persys.control.v1.CloudInitConfig + 0, // 23: persys.control.v1.WorkloadStatus.failure_reason:type_name -> persys.control.v1.FailureReason + 43, // 24: persys.control.v1.WorkloadStatus.last_transition:type_name -> google.protobuf.Timestamp + 29, // 25: persys.control.v1.ListNodesResponse.nodes:type_name -> persys.control.v1.NodeView + 29, // 26: persys.control.v1.GetNodeResponse.node:type_name -> persys.control.v1.NodeView + 43, // 27: persys.control.v1.NodeView.status_updated_at:type_name -> google.protobuf.Timestamp + 43, // 28: persys.control.v1.NodeView.last_heartbeat:type_name -> google.protobuf.Timestamp + 42, // 29: persys.control.v1.NodeView.labels:type_name -> persys.control.v1.NodeView.LabelsEntry + 34, // 30: persys.control.v1.ListWorkloadsResponse.workloads:type_name -> persys.control.v1.WorkloadView + 34, // 31: persys.control.v1.GetWorkloadResponse.workload:type_name -> persys.control.v1.WorkloadView + 43, // 32: persys.control.v1.WorkloadView.retry_next_at:type_name -> google.protobuf.Timestamp + 43, // 33: persys.control.v1.WorkloadView.last_updated:type_name -> google.protobuf.Timestamp + 43, // 34: persys.control.v1.GetClusterSummaryResponse.generated_at:type_name -> google.protobuf.Timestamp + 1, // 35: persys.control.v1.ControlMessage.register:type_name -> persys.control.v1.RegisterNodeRequest + 5, // 36: persys.control.v1.ControlMessage.heartbeat:type_name -> persys.control.v1.HeartbeatRequest + 8, // 37: persys.control.v1.ControlMessage.apply:type_name -> persys.control.v1.ApplyWorkloadRequest + 10, // 38: persys.control.v1.ControlMessage.delete:type_name -> persys.control.v1.DeleteWorkloadRequest + 1, // 39: persys.control.v1.AgentControl.RegisterNode:input_type -> persys.control.v1.RegisterNodeRequest + 5, // 40: persys.control.v1.AgentControl.Heartbeat:input_type -> persys.control.v1.HeartbeatRequest + 8, // 41: persys.control.v1.AgentControl.ApplyWorkload:input_type -> persys.control.v1.ApplyWorkloadRequest + 10, // 42: persys.control.v1.AgentControl.DeleteWorkload:input_type -> persys.control.v1.DeleteWorkloadRequest + 23, // 43: persys.control.v1.AgentControl.RetryWorkload:input_type -> persys.control.v1.RetryWorkloadRequest + 25, // 44: persys.control.v1.AgentControl.ListNodes:input_type -> persys.control.v1.ListNodesRequest + 26, // 45: persys.control.v1.AgentControl.GetNode:input_type -> persys.control.v1.GetNodeRequest + 30, // 46: persys.control.v1.AgentControl.ListWorkloads:input_type -> persys.control.v1.ListWorkloadsRequest + 31, // 47: persys.control.v1.AgentControl.GetWorkload:input_type -> persys.control.v1.GetWorkloadRequest + 35, // 48: persys.control.v1.AgentControl.GetClusterSummary:input_type -> persys.control.v1.GetClusterSummaryRequest + 37, // 49: persys.control.v1.AgentControl.ControlStream:input_type -> persys.control.v1.ControlMessage + 4, // 50: persys.control.v1.AgentControl.RegisterNode:output_type -> persys.control.v1.RegisterNodeResponse + 7, // 51: persys.control.v1.AgentControl.Heartbeat:output_type -> persys.control.v1.HeartbeatResponse + 9, // 52: persys.control.v1.AgentControl.ApplyWorkload:output_type -> persys.control.v1.ApplyWorkloadResponse + 11, // 53: persys.control.v1.AgentControl.DeleteWorkload:output_type -> persys.control.v1.DeleteWorkloadResponse + 24, // 54: persys.control.v1.AgentControl.RetryWorkload:output_type -> persys.control.v1.RetryWorkloadResponse + 27, // 55: persys.control.v1.AgentControl.ListNodes:output_type -> persys.control.v1.ListNodesResponse + 28, // 56: persys.control.v1.AgentControl.GetNode:output_type -> persys.control.v1.GetNodeResponse + 32, // 57: persys.control.v1.AgentControl.ListWorkloads:output_type -> persys.control.v1.ListWorkloadsResponse + 33, // 58: persys.control.v1.AgentControl.GetWorkload:output_type -> persys.control.v1.GetWorkloadResponse + 36, // 59: persys.control.v1.AgentControl.GetClusterSummary:output_type -> persys.control.v1.GetClusterSummaryResponse + 37, // 60: persys.control.v1.AgentControl.ControlStream:output_type -> persys.control.v1.ControlMessage + 50, // [50:61] is the sub-list for method output_type + 39, // [39:50] is the sub-list for method input_type + 39, // [39:39] is the sub-list for extension type_name + 39, // [39:39] is the sub-list for extension extendee + 0, // [0:39] is the sub-list for field type_name +} + +func init() { file_control_proto_init() } +func file_control_proto_init() { + if File_control_proto != nil { + return + } + file_control_proto_msgTypes[11].OneofWrappers = []any{ + (*WorkloadSpec_Container)(nil), + (*WorkloadSpec_Compose)(nil), + (*WorkloadSpec_Vm)(nil), + } + file_control_proto_msgTypes[36].OneofWrappers = []any{ + (*ControlMessage_Register)(nil), + (*ControlMessage_Heartbeat)(nil), + (*ControlMessage_Apply)(nil), + (*ControlMessage_Delete)(nil), + } + type x struct{} + out := protoimpl.TypeBuilder{ + File: protoimpl.DescBuilder{ + GoPackagePath: reflect.TypeOf(x{}).PkgPath(), + RawDescriptor: unsafe.Slice(unsafe.StringData(file_control_proto_rawDesc), len(file_control_proto_rawDesc)), + NumEnums: 1, + NumMessages: 42, + NumExtensions: 0, + NumServices: 1, + }, + GoTypes: file_control_proto_goTypes, + DependencyIndexes: file_control_proto_depIdxs, + EnumInfos: file_control_proto_enumTypes, + MessageInfos: file_control_proto_msgTypes, + }.Build() + File_control_proto = out.File + file_control_proto_goTypes = nil + file_control_proto_depIdxs = nil +} diff --git a/persys-scheduler/internal/controlv1/control_grpc.pb.go b/persys-scheduler/internal/controlv1/control_grpc.pb.go new file mode 100644 index 0000000..e5c7bfe --- /dev/null +++ b/persys-scheduler/internal/controlv1/control_grpc.pb.go @@ -0,0 +1,508 @@ +// Code generated by protoc-gen-go-grpc. DO NOT EDIT. +// versions: +// - protoc-gen-go-grpc v1.6.1 +// - protoc v3.21.12 +// source: control.proto + +package controlv1 + +import ( + context "context" + grpc "google.golang.org/grpc" + codes "google.golang.org/grpc/codes" + status "google.golang.org/grpc/status" +) + +// This is a compile-time assertion to ensure that this generated file +// is compatible with the grpc package it is being compiled against. +// Requires gRPC-Go v1.64.0 or later. +const _ = grpc.SupportPackageIsVersion9 + +const ( + AgentControl_RegisterNode_FullMethodName = "/persys.control.v1.AgentControl/RegisterNode" + AgentControl_Heartbeat_FullMethodName = "/persys.control.v1.AgentControl/Heartbeat" + AgentControl_ApplyWorkload_FullMethodName = "/persys.control.v1.AgentControl/ApplyWorkload" + AgentControl_DeleteWorkload_FullMethodName = "/persys.control.v1.AgentControl/DeleteWorkload" + AgentControl_RetryWorkload_FullMethodName = "/persys.control.v1.AgentControl/RetryWorkload" + AgentControl_ListNodes_FullMethodName = "/persys.control.v1.AgentControl/ListNodes" + AgentControl_GetNode_FullMethodName = "/persys.control.v1.AgentControl/GetNode" + AgentControl_ListWorkloads_FullMethodName = "/persys.control.v1.AgentControl/ListWorkloads" + AgentControl_GetWorkload_FullMethodName = "/persys.control.v1.AgentControl/GetWorkload" + AgentControl_GetClusterSummary_FullMethodName = "/persys.control.v1.AgentControl/GetClusterSummary" + AgentControl_ControlStream_FullMethodName = "/persys.control.v1.AgentControl/ControlStream" +) + +// AgentControlClient is the client API for AgentControl service. +// +// For semantics around ctx use and closing/ending streaming RPCs, please refer to https://pkg.go.dev/google.golang.org/grpc/?tab=doc#ClientConn.NewStream. +type AgentControlClient interface { + // Registration + RegisterNode(ctx context.Context, in *RegisterNodeRequest, opts ...grpc.CallOption) (*RegisterNodeResponse, error) + // Heartbeat + Heartbeat(ctx context.Context, in *HeartbeatRequest, opts ...grpc.CallOption) (*HeartbeatResponse, error) + // Workload lifecycle + ApplyWorkload(ctx context.Context, in *ApplyWorkloadRequest, opts ...grpc.CallOption) (*ApplyWorkloadResponse, error) + DeleteWorkload(ctx context.Context, in *DeleteWorkloadRequest, opts ...grpc.CallOption) (*DeleteWorkloadResponse, error) + // Retry trigger + RetryWorkload(ctx context.Context, in *RetryWorkloadRequest, opts ...grpc.CallOption) (*RetryWorkloadResponse, error) + // Cluster and node management visibility + ListNodes(ctx context.Context, in *ListNodesRequest, opts ...grpc.CallOption) (*ListNodesResponse, error) + GetNode(ctx context.Context, in *GetNodeRequest, opts ...grpc.CallOption) (*GetNodeResponse, error) + ListWorkloads(ctx context.Context, in *ListWorkloadsRequest, opts ...grpc.CallOption) (*ListWorkloadsResponse, error) + GetWorkload(ctx context.Context, in *GetWorkloadRequest, opts ...grpc.CallOption) (*GetWorkloadResponse, error) + GetClusterSummary(ctx context.Context, in *GetClusterSummaryRequest, opts ...grpc.CallOption) (*GetClusterSummaryResponse, error) + // Optional future streaming channel + ControlStream(ctx context.Context, opts ...grpc.CallOption) (grpc.BidiStreamingClient[ControlMessage, ControlMessage], error) +} + +type agentControlClient struct { + cc grpc.ClientConnInterface +} + +func NewAgentControlClient(cc grpc.ClientConnInterface) AgentControlClient { + return &agentControlClient{cc} +} + +func (c *agentControlClient) RegisterNode(ctx context.Context, in *RegisterNodeRequest, opts ...grpc.CallOption) (*RegisterNodeResponse, error) { + cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...) + out := new(RegisterNodeResponse) + err := c.cc.Invoke(ctx, AgentControl_RegisterNode_FullMethodName, in, out, cOpts...) + if err != nil { + return nil, err + } + return out, nil +} + +func (c *agentControlClient) Heartbeat(ctx context.Context, in *HeartbeatRequest, opts ...grpc.CallOption) (*HeartbeatResponse, error) { + cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...) + out := new(HeartbeatResponse) + err := c.cc.Invoke(ctx, AgentControl_Heartbeat_FullMethodName, in, out, cOpts...) + if err != nil { + return nil, err + } + return out, nil +} + +func (c *agentControlClient) ApplyWorkload(ctx context.Context, in *ApplyWorkloadRequest, opts ...grpc.CallOption) (*ApplyWorkloadResponse, error) { + cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...) + out := new(ApplyWorkloadResponse) + err := c.cc.Invoke(ctx, AgentControl_ApplyWorkload_FullMethodName, in, out, cOpts...) + if err != nil { + return nil, err + } + return out, nil +} + +func (c *agentControlClient) DeleteWorkload(ctx context.Context, in *DeleteWorkloadRequest, opts ...grpc.CallOption) (*DeleteWorkloadResponse, error) { + cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...) + out := new(DeleteWorkloadResponse) + err := c.cc.Invoke(ctx, AgentControl_DeleteWorkload_FullMethodName, in, out, cOpts...) + if err != nil { + return nil, err + } + return out, nil +} + +func (c *agentControlClient) RetryWorkload(ctx context.Context, in *RetryWorkloadRequest, opts ...grpc.CallOption) (*RetryWorkloadResponse, error) { + cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...) + out := new(RetryWorkloadResponse) + err := c.cc.Invoke(ctx, AgentControl_RetryWorkload_FullMethodName, in, out, cOpts...) + if err != nil { + return nil, err + } + return out, nil +} + +func (c *agentControlClient) ListNodes(ctx context.Context, in *ListNodesRequest, opts ...grpc.CallOption) (*ListNodesResponse, error) { + cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...) + out := new(ListNodesResponse) + err := c.cc.Invoke(ctx, AgentControl_ListNodes_FullMethodName, in, out, cOpts...) + if err != nil { + return nil, err + } + return out, nil +} + +func (c *agentControlClient) GetNode(ctx context.Context, in *GetNodeRequest, opts ...grpc.CallOption) (*GetNodeResponse, error) { + cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...) + out := new(GetNodeResponse) + err := c.cc.Invoke(ctx, AgentControl_GetNode_FullMethodName, in, out, cOpts...) + if err != nil { + return nil, err + } + return out, nil +} + +func (c *agentControlClient) ListWorkloads(ctx context.Context, in *ListWorkloadsRequest, opts ...grpc.CallOption) (*ListWorkloadsResponse, error) { + cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...) + out := new(ListWorkloadsResponse) + err := c.cc.Invoke(ctx, AgentControl_ListWorkloads_FullMethodName, in, out, cOpts...) + if err != nil { + return nil, err + } + return out, nil +} + +func (c *agentControlClient) GetWorkload(ctx context.Context, in *GetWorkloadRequest, opts ...grpc.CallOption) (*GetWorkloadResponse, error) { + cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...) + out := new(GetWorkloadResponse) + err := c.cc.Invoke(ctx, AgentControl_GetWorkload_FullMethodName, in, out, cOpts...) + if err != nil { + return nil, err + } + return out, nil +} + +func (c *agentControlClient) GetClusterSummary(ctx context.Context, in *GetClusterSummaryRequest, opts ...grpc.CallOption) (*GetClusterSummaryResponse, error) { + cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...) + out := new(GetClusterSummaryResponse) + err := c.cc.Invoke(ctx, AgentControl_GetClusterSummary_FullMethodName, in, out, cOpts...) + if err != nil { + return nil, err + } + return out, nil +} + +func (c *agentControlClient) ControlStream(ctx context.Context, opts ...grpc.CallOption) (grpc.BidiStreamingClient[ControlMessage, ControlMessage], error) { + cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...) + stream, err := c.cc.NewStream(ctx, &AgentControl_ServiceDesc.Streams[0], AgentControl_ControlStream_FullMethodName, cOpts...) + if err != nil { + return nil, err + } + x := &grpc.GenericClientStream[ControlMessage, ControlMessage]{ClientStream: stream} + return x, nil +} + +// This type alias is provided for backwards compatibility with existing code that references the prior non-generic stream type by name. +type AgentControl_ControlStreamClient = grpc.BidiStreamingClient[ControlMessage, ControlMessage] + +// AgentControlServer is the server API for AgentControl service. +// All implementations must embed UnimplementedAgentControlServer +// for forward compatibility. +type AgentControlServer interface { + // Registration + RegisterNode(context.Context, *RegisterNodeRequest) (*RegisterNodeResponse, error) + // Heartbeat + Heartbeat(context.Context, *HeartbeatRequest) (*HeartbeatResponse, error) + // Workload lifecycle + ApplyWorkload(context.Context, *ApplyWorkloadRequest) (*ApplyWorkloadResponse, error) + DeleteWorkload(context.Context, *DeleteWorkloadRequest) (*DeleteWorkloadResponse, error) + // Retry trigger + RetryWorkload(context.Context, *RetryWorkloadRequest) (*RetryWorkloadResponse, error) + // Cluster and node management visibility + ListNodes(context.Context, *ListNodesRequest) (*ListNodesResponse, error) + GetNode(context.Context, *GetNodeRequest) (*GetNodeResponse, error) + ListWorkloads(context.Context, *ListWorkloadsRequest) (*ListWorkloadsResponse, error) + GetWorkload(context.Context, *GetWorkloadRequest) (*GetWorkloadResponse, error) + GetClusterSummary(context.Context, *GetClusterSummaryRequest) (*GetClusterSummaryResponse, error) + // Optional future streaming channel + ControlStream(grpc.BidiStreamingServer[ControlMessage, ControlMessage]) error + mustEmbedUnimplementedAgentControlServer() +} + +// UnimplementedAgentControlServer must be embedded to have +// forward compatible implementations. +// +// NOTE: this should be embedded by value instead of pointer to avoid a nil +// pointer dereference when methods are called. +type UnimplementedAgentControlServer struct{} + +func (UnimplementedAgentControlServer) RegisterNode(context.Context, *RegisterNodeRequest) (*RegisterNodeResponse, error) { + return nil, status.Error(codes.Unimplemented, "method RegisterNode not implemented") +} +func (UnimplementedAgentControlServer) Heartbeat(context.Context, *HeartbeatRequest) (*HeartbeatResponse, error) { + return nil, status.Error(codes.Unimplemented, "method Heartbeat not implemented") +} +func (UnimplementedAgentControlServer) ApplyWorkload(context.Context, *ApplyWorkloadRequest) (*ApplyWorkloadResponse, error) { + return nil, status.Error(codes.Unimplemented, "method ApplyWorkload not implemented") +} +func (UnimplementedAgentControlServer) DeleteWorkload(context.Context, *DeleteWorkloadRequest) (*DeleteWorkloadResponse, error) { + return nil, status.Error(codes.Unimplemented, "method DeleteWorkload not implemented") +} +func (UnimplementedAgentControlServer) RetryWorkload(context.Context, *RetryWorkloadRequest) (*RetryWorkloadResponse, error) { + return nil, status.Error(codes.Unimplemented, "method RetryWorkload not implemented") +} +func (UnimplementedAgentControlServer) ListNodes(context.Context, *ListNodesRequest) (*ListNodesResponse, error) { + return nil, status.Error(codes.Unimplemented, "method ListNodes not implemented") +} +func (UnimplementedAgentControlServer) GetNode(context.Context, *GetNodeRequest) (*GetNodeResponse, error) { + return nil, status.Error(codes.Unimplemented, "method GetNode not implemented") +} +func (UnimplementedAgentControlServer) ListWorkloads(context.Context, *ListWorkloadsRequest) (*ListWorkloadsResponse, error) { + return nil, status.Error(codes.Unimplemented, "method ListWorkloads not implemented") +} +func (UnimplementedAgentControlServer) GetWorkload(context.Context, *GetWorkloadRequest) (*GetWorkloadResponse, error) { + return nil, status.Error(codes.Unimplemented, "method GetWorkload not implemented") +} +func (UnimplementedAgentControlServer) GetClusterSummary(context.Context, *GetClusterSummaryRequest) (*GetClusterSummaryResponse, error) { + return nil, status.Error(codes.Unimplemented, "method GetClusterSummary not implemented") +} +func (UnimplementedAgentControlServer) ControlStream(grpc.BidiStreamingServer[ControlMessage, ControlMessage]) error { + return status.Error(codes.Unimplemented, "method ControlStream not implemented") +} +func (UnimplementedAgentControlServer) mustEmbedUnimplementedAgentControlServer() {} +func (UnimplementedAgentControlServer) testEmbeddedByValue() {} + +// UnsafeAgentControlServer may be embedded to opt out of forward compatibility for this service. +// Use of this interface is not recommended, as added methods to AgentControlServer will +// result in compilation errors. +type UnsafeAgentControlServer interface { + mustEmbedUnimplementedAgentControlServer() +} + +func RegisterAgentControlServer(s grpc.ServiceRegistrar, srv AgentControlServer) { + // If the following call panics, it indicates UnimplementedAgentControlServer was + // embedded by pointer and is nil. This will cause panics if an + // unimplemented method is ever invoked, so we test this at initialization + // time to prevent it from happening at runtime later due to I/O. + if t, ok := srv.(interface{ testEmbeddedByValue() }); ok { + t.testEmbeddedByValue() + } + s.RegisterService(&AgentControl_ServiceDesc, srv) +} + +func _AgentControl_RegisterNode_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { + in := new(RegisterNodeRequest) + if err := dec(in); err != nil { + return nil, err + } + if interceptor == nil { + return srv.(AgentControlServer).RegisterNode(ctx, in) + } + info := &grpc.UnaryServerInfo{ + Server: srv, + FullMethod: AgentControl_RegisterNode_FullMethodName, + } + handler := func(ctx context.Context, req interface{}) (interface{}, error) { + return srv.(AgentControlServer).RegisterNode(ctx, req.(*RegisterNodeRequest)) + } + return interceptor(ctx, in, info, handler) +} + +func _AgentControl_Heartbeat_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { + in := new(HeartbeatRequest) + if err := dec(in); err != nil { + return nil, err + } + if interceptor == nil { + return srv.(AgentControlServer).Heartbeat(ctx, in) + } + info := &grpc.UnaryServerInfo{ + Server: srv, + FullMethod: AgentControl_Heartbeat_FullMethodName, + } + handler := func(ctx context.Context, req interface{}) (interface{}, error) { + return srv.(AgentControlServer).Heartbeat(ctx, req.(*HeartbeatRequest)) + } + return interceptor(ctx, in, info, handler) +} + +func _AgentControl_ApplyWorkload_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { + in := new(ApplyWorkloadRequest) + if err := dec(in); err != nil { + return nil, err + } + if interceptor == nil { + return srv.(AgentControlServer).ApplyWorkload(ctx, in) + } + info := &grpc.UnaryServerInfo{ + Server: srv, + FullMethod: AgentControl_ApplyWorkload_FullMethodName, + } + handler := func(ctx context.Context, req interface{}) (interface{}, error) { + return srv.(AgentControlServer).ApplyWorkload(ctx, req.(*ApplyWorkloadRequest)) + } + return interceptor(ctx, in, info, handler) +} + +func _AgentControl_DeleteWorkload_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { + in := new(DeleteWorkloadRequest) + if err := dec(in); err != nil { + return nil, err + } + if interceptor == nil { + return srv.(AgentControlServer).DeleteWorkload(ctx, in) + } + info := &grpc.UnaryServerInfo{ + Server: srv, + FullMethod: AgentControl_DeleteWorkload_FullMethodName, + } + handler := func(ctx context.Context, req interface{}) (interface{}, error) { + return srv.(AgentControlServer).DeleteWorkload(ctx, req.(*DeleteWorkloadRequest)) + } + return interceptor(ctx, in, info, handler) +} + +func _AgentControl_RetryWorkload_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { + in := new(RetryWorkloadRequest) + if err := dec(in); err != nil { + return nil, err + } + if interceptor == nil { + return srv.(AgentControlServer).RetryWorkload(ctx, in) + } + info := &grpc.UnaryServerInfo{ + Server: srv, + FullMethod: AgentControl_RetryWorkload_FullMethodName, + } + handler := func(ctx context.Context, req interface{}) (interface{}, error) { + return srv.(AgentControlServer).RetryWorkload(ctx, req.(*RetryWorkloadRequest)) + } + return interceptor(ctx, in, info, handler) +} + +func _AgentControl_ListNodes_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { + in := new(ListNodesRequest) + if err := dec(in); err != nil { + return nil, err + } + if interceptor == nil { + return srv.(AgentControlServer).ListNodes(ctx, in) + } + info := &grpc.UnaryServerInfo{ + Server: srv, + FullMethod: AgentControl_ListNodes_FullMethodName, + } + handler := func(ctx context.Context, req interface{}) (interface{}, error) { + return srv.(AgentControlServer).ListNodes(ctx, req.(*ListNodesRequest)) + } + return interceptor(ctx, in, info, handler) +} + +func _AgentControl_GetNode_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { + in := new(GetNodeRequest) + if err := dec(in); err != nil { + return nil, err + } + if interceptor == nil { + return srv.(AgentControlServer).GetNode(ctx, in) + } + info := &grpc.UnaryServerInfo{ + Server: srv, + FullMethod: AgentControl_GetNode_FullMethodName, + } + handler := func(ctx context.Context, req interface{}) (interface{}, error) { + return srv.(AgentControlServer).GetNode(ctx, req.(*GetNodeRequest)) + } + return interceptor(ctx, in, info, handler) +} + +func _AgentControl_ListWorkloads_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { + in := new(ListWorkloadsRequest) + if err := dec(in); err != nil { + return nil, err + } + if interceptor == nil { + return srv.(AgentControlServer).ListWorkloads(ctx, in) + } + info := &grpc.UnaryServerInfo{ + Server: srv, + FullMethod: AgentControl_ListWorkloads_FullMethodName, + } + handler := func(ctx context.Context, req interface{}) (interface{}, error) { + return srv.(AgentControlServer).ListWorkloads(ctx, req.(*ListWorkloadsRequest)) + } + return interceptor(ctx, in, info, handler) +} + +func _AgentControl_GetWorkload_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { + in := new(GetWorkloadRequest) + if err := dec(in); err != nil { + return nil, err + } + if interceptor == nil { + return srv.(AgentControlServer).GetWorkload(ctx, in) + } + info := &grpc.UnaryServerInfo{ + Server: srv, + FullMethod: AgentControl_GetWorkload_FullMethodName, + } + handler := func(ctx context.Context, req interface{}) (interface{}, error) { + return srv.(AgentControlServer).GetWorkload(ctx, req.(*GetWorkloadRequest)) + } + return interceptor(ctx, in, info, handler) +} + +func _AgentControl_GetClusterSummary_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { + in := new(GetClusterSummaryRequest) + if err := dec(in); err != nil { + return nil, err + } + if interceptor == nil { + return srv.(AgentControlServer).GetClusterSummary(ctx, in) + } + info := &grpc.UnaryServerInfo{ + Server: srv, + FullMethod: AgentControl_GetClusterSummary_FullMethodName, + } + handler := func(ctx context.Context, req interface{}) (interface{}, error) { + return srv.(AgentControlServer).GetClusterSummary(ctx, req.(*GetClusterSummaryRequest)) + } + return interceptor(ctx, in, info, handler) +} + +func _AgentControl_ControlStream_Handler(srv interface{}, stream grpc.ServerStream) error { + return srv.(AgentControlServer).ControlStream(&grpc.GenericServerStream[ControlMessage, ControlMessage]{ServerStream: stream}) +} + +// This type alias is provided for backwards compatibility with existing code that references the prior non-generic stream type by name. +type AgentControl_ControlStreamServer = grpc.BidiStreamingServer[ControlMessage, ControlMessage] + +// AgentControl_ServiceDesc is the grpc.ServiceDesc for AgentControl service. +// It's only intended for direct use with grpc.RegisterService, +// and not to be introspected or modified (even as a copy) +var AgentControl_ServiceDesc = grpc.ServiceDesc{ + ServiceName: "persys.control.v1.AgentControl", + HandlerType: (*AgentControlServer)(nil), + Methods: []grpc.MethodDesc{ + { + MethodName: "RegisterNode", + Handler: _AgentControl_RegisterNode_Handler, + }, + { + MethodName: "Heartbeat", + Handler: _AgentControl_Heartbeat_Handler, + }, + { + MethodName: "ApplyWorkload", + Handler: _AgentControl_ApplyWorkload_Handler, + }, + { + MethodName: "DeleteWorkload", + Handler: _AgentControl_DeleteWorkload_Handler, + }, + { + MethodName: "RetryWorkload", + Handler: _AgentControl_RetryWorkload_Handler, + }, + { + MethodName: "ListNodes", + Handler: _AgentControl_ListNodes_Handler, + }, + { + MethodName: "GetNode", + Handler: _AgentControl_GetNode_Handler, + }, + { + MethodName: "ListWorkloads", + Handler: _AgentControl_ListWorkloads_Handler, + }, + { + MethodName: "GetWorkload", + Handler: _AgentControl_GetWorkload_Handler, + }, + { + MethodName: "GetClusterSummary", + Handler: _AgentControl_GetClusterSummary_Handler, + }, + }, + Streams: []grpc.StreamDesc{ + { + StreamName: "ControlStream", + Handler: _AgentControl_ControlStream_Handler, + ServerStreams: true, + ClientStreams: true, + }, + }, + Metadata: "control.proto", +} diff --git a/persys-scheduler/internal/grpcapi/service.go b/persys-scheduler/internal/grpcapi/service.go new file mode 100644 index 0000000..7ecf78f --- /dev/null +++ b/persys-scheduler/internal/grpcapi/service.go @@ -0,0 +1,604 @@ +package grpcapi + +import ( + "context" + "fmt" + "strconv" + "strings" + "time" + + controlv1 "github.com/persys-dev/persys-cloud/persys-scheduler/internal/controlv1" + "github.com/persys-dev/persys-cloud/persys-scheduler/internal/models" + "github.com/persys-dev/persys-cloud/persys-scheduler/internal/scheduler" + "go.opentelemetry.io/otel/attribute" + otelcodes "go.opentelemetry.io/otel/codes" + "go.opentelemetry.io/otel/trace" + "google.golang.org/grpc/codes" + "google.golang.org/grpc/status" + "google.golang.org/protobuf/types/known/timestamppb" +) + +type Service struct { + controlv1.UnimplementedAgentControlServer + sched *scheduler.Scheduler +} + +func NewService(sched *scheduler.Scheduler) *Service { + return &Service{sched: sched} +} + +func annotateRPC(ctx context.Context, attrs ...attribute.KeyValue) { + span := trace.SpanFromContext(ctx) + if span == nil { + return + } + span.SetAttributes(attrs...) +} + +func recordRPCError(ctx context.Context, err error) { + if err == nil { + return + } + span := trace.SpanFromContext(ctx) + if span == nil { + return + } + span.RecordError(err) + span.SetStatus(otelcodes.Error, err.Error()) +} + +func (s *Service) RegisterNode(ctx context.Context, in *controlv1.RegisterNodeRequest) (*controlv1.RegisterNodeResponse, error) { + if in != nil { + annotateRPC(ctx, attribute.String("scheduler.node_id", strings.TrimSpace(in.GetNodeId()))) + } + if in == nil || strings.TrimSpace(in.GetNodeId()) == "" { + err := status.Error(codes.InvalidArgument, "node_id is required") + recordRPCError(ctx, err) + return nil, err + } + + node := models.Node{ + NodeID: in.GetNodeId(), + Status: "Ready", + Labels: in.GetLabels(), + Timestamp: in.GetTimestamp().AsTime().UTC().Format(time.RFC3339), + AvailableCPU: float64(in.GetCapabilities().GetCpuTotalMillicores()) / 1000.0, + TotalCPU: float64(in.GetCapabilities().GetCpuTotalMillicores()) / 1000.0, + AvailableMemory: in.GetCapabilities().GetMemoryTotalMb(), + TotalMemory: in.GetCapabilities().GetMemoryTotalMb(), + SupportedWorkloadTypes: normalizeSupportedWorkloadTypes(in.GetCapabilities().GetSupportedWorkloadTypes()), + } + + if endpoint := strings.TrimSpace(in.GetGrpcEndpoint()); endpoint != "" { + node.AgentEndpoint = endpoint + host, port, err := splitHostPort(endpoint) + if err != nil { + rpcErr := status.Errorf(codes.InvalidArgument, "invalid grpc_endpoint %q: %v", endpoint, err) + recordRPCError(ctx, rpcErr) + return nil, rpcErr + } + node.IPAddress = host + node.AgentGRPCPort = port + node.AgentPort = port + } + if node.IPAddress == "" { + err := status.Error(codes.InvalidArgument, "grpc_endpoint is required") + recordRPCError(ctx, err) + return nil, err + } + if node.AgentPort == 0 { + node.AgentPort = 50051 + node.AgentGRPCPort = 50051 + } + if node.Username == "" { + node.Username = "agent" + } + if node.Hostname == "" { + node.Hostname = in.GetNodeId() + } + if node.OSName == "" { + node.OSName = "linux" + } + if node.KernelVersion == "" { + node.KernelVersion = "unknown" + } + + if err := s.sched.RegisterNode(node); err != nil { + return &controlv1.RegisterNodeResponse{Accepted: false, Reason: err.Error()}, nil + } + + now := time.Now().UTC() + interval := int32(60) + lease := now.Add(time.Duration(interval*3) * time.Second) + return &controlv1.RegisterNodeResponse{ + Accepted: true, + Reason: "registered", + HeartbeatIntervalSeconds: interval, + LeaseExpiresAt: timestamppb.New(lease), + }, nil +} + +func (s *Service) Heartbeat(ctx context.Context, in *controlv1.HeartbeatRequest) (*controlv1.HeartbeatResponse, error) { + if in != nil { + annotateRPC(ctx, attribute.String("scheduler.node_id", strings.TrimSpace(in.GetNodeId()))) + } + if in == nil || strings.TrimSpace(in.GetNodeId()) == "" { + err := status.Error(codes.InvalidArgument, "node_id is required") + recordRPCError(ctx, err) + return nil, err + } + + currentNode, _ := s.sched.GetNodeByID(in.GetNodeId()) + + availableCPU := currentNode.AvailableCPU + availableMemory := currentNode.AvailableMemory + if in.GetUsage() != nil { + totalMillicores := int64(currentNode.TotalCPU * 1000.0) + if totalMillicores > 0 { + availableCPU = float64(maxInt64(0, totalMillicores-in.GetUsage().GetCpuUsedMillicores())) / 1000.0 + } + if currentNode.TotalMemory > 0 { + availableMemory = maxInt64(0, currentNode.TotalMemory-in.GetUsage().GetMemoryUsedMb()) + } + } + + if err := s.sched.UpdateNodeHeartbeat(in.GetNodeId(), "Ready", availableCPU, availableMemory); err != nil { + rpcErr := status.Error(codes.Internal, err.Error()) + recordRPCError(ctx, rpcErr) + return nil, rpcErr + } + + for _, ws := range in.GetWorkloadStatuses() { + if strings.TrimSpace(ws.GetWorkloadId()) == "" { + continue + } + _ = s.sched.UpdateWorkloadStatus(ws.GetWorkloadId(), ws.GetState()) + if msg := strings.TrimSpace(ws.GetMessage()); msg != "" { + _ = s.sched.UpdateWorkloadLogs(ws.GetWorkloadId(), msg) + } + } + + lease := time.Now().UTC().Add(3 * time.Minute) + return &controlv1.HeartbeatResponse{Acknowledged: true, DrainNode: false, LeaseExpiresAt: timestamppb.New(lease)}, nil +} + +func (s *Service) ApplyWorkload(ctx context.Context, in *controlv1.ApplyWorkloadRequest) (*controlv1.ApplyWorkloadResponse, error) { + if in != nil { + annotateRPC(ctx, + attribute.String("scheduler.workload_id", strings.TrimSpace(in.GetWorkloadId())), + attribute.String("scheduler.workload_desired_state", strings.TrimSpace(in.GetDesiredState())), + ) + } + if in == nil || strings.TrimSpace(in.GetWorkloadId()) == "" { + err := status.Error(codes.InvalidArgument, "workload_id is required") + recordRPCError(ctx, err) + return nil, err + } + workload, err := controlApplyToModel(in) + if err != nil { + annotateRPC(ctx, attribute.String("scheduler.workload_type", strings.TrimSpace(in.GetSpec().GetType()))) + return &controlv1.ApplyWorkloadResponse{Success: false, FailureReason: controlv1.FailureReason_INVALID_SPEC, ErrorMessage: err.Error()}, nil + } + annotateRPC(ctx, attribute.String("scheduler.workload_type", strings.TrimSpace(workload.Type))) + + var persisted models.Workload + if _, err := s.sched.GetWorkloadByID(workload.ID); err == nil { + updated, err := s.sched.UpdateWorkloadSpec(workload.ID, workload) + if err != nil { + return &controlv1.ApplyWorkloadResponse{Success: false, FailureReason: controlv1.FailureReason_RUNTIME_ERROR, ErrorMessage: err.Error()}, nil + } + persisted = updated + } else { + created, err := s.sched.CreateWorkload(workload) + if err != nil { + return &controlv1.ApplyWorkloadResponse{Success: false, FailureReason: controlv1.FailureReason_RUNTIME_ERROR, ErrorMessage: err.Error()}, nil + } + persisted = created + } + + // Trigger reconciliation immediately so ApplyWorkload performs scheduling now, + // instead of waiting for the periodic reconciliation tick. + if _, err := s.sched.ReconcileWorkloadWithContext(ctx, persisted); err != nil { + return &controlv1.ApplyWorkloadResponse{Success: false, FailureReason: controlv1.FailureReason_RUNTIME_ERROR, ErrorMessage: err.Error()}, nil + } + return &controlv1.ApplyWorkloadResponse{Success: true}, nil +} + +func (s *Service) DeleteWorkload(ctx context.Context, in *controlv1.DeleteWorkloadRequest) (*controlv1.DeleteWorkloadResponse, error) { + if in != nil { + annotateRPC(ctx, attribute.String("scheduler.workload_id", strings.TrimSpace(in.GetWorkloadId()))) + } + if in == nil || strings.TrimSpace(in.GetWorkloadId()) == "" { + err := status.Error(codes.InvalidArgument, "workload_id is required") + recordRPCError(ctx, err) + return nil, err + } + if err := s.sched.MarkWorkloadDeleted(in.GetWorkloadId()); err != nil { + return &controlv1.DeleteWorkloadResponse{Success: false, ErrorMessage: err.Error()}, nil + } + return &controlv1.DeleteWorkloadResponse{Success: true}, nil +} + +func (s *Service) RetryWorkload(ctx context.Context, in *controlv1.RetryWorkloadRequest) (*controlv1.RetryWorkloadResponse, error) { + if in != nil { + annotateRPC(ctx, attribute.String("scheduler.workload_id", strings.TrimSpace(in.GetWorkloadId()))) + } + if in == nil || strings.TrimSpace(in.GetWorkloadId()) == "" { + err := status.Error(codes.InvalidArgument, "workload_id is required") + recordRPCError(ctx, err) + return nil, err + } + if _, err := s.sched.TriggerWorkloadRetry(in.GetWorkloadId()); err != nil { + return &controlv1.RetryWorkloadResponse{Accepted: false}, nil + } + return &controlv1.RetryWorkloadResponse{Accepted: true}, nil +} + +func (s *Service) ListNodes(ctx context.Context, in *controlv1.ListNodesRequest) (*controlv1.ListNodesResponse, error) { + if in != nil { + annotateRPC(ctx, attribute.String("scheduler.filter_status", strings.TrimSpace(in.GetStatus()))) + } + nodes, err := s.sched.GetNodes() + if err != nil { + rpcErr := status.Error(codes.Internal, err.Error()) + recordRPCError(ctx, rpcErr) + return nil, rpcErr + } + + filterStatus := strings.ToLower(strings.TrimSpace(in.GetStatus())) + out := make([]*controlv1.NodeView, 0, len(nodes)) + for _, node := range nodes { + if filterStatus != "" && strings.ToLower(strings.TrimSpace(node.Status)) != filterStatus { + continue + } + out = append(out, nodeToView(node)) + } + return &controlv1.ListNodesResponse{Nodes: out}, nil +} + +func (s *Service) GetNode(ctx context.Context, in *controlv1.GetNodeRequest) (*controlv1.GetNodeResponse, error) { + if in != nil { + annotateRPC(ctx, attribute.String("scheduler.node_id", strings.TrimSpace(in.GetNodeId()))) + } + if in == nil || strings.TrimSpace(in.GetNodeId()) == "" { + err := status.Error(codes.InvalidArgument, "node_id is required") + recordRPCError(ctx, err) + return nil, err + } + node, err := s.sched.GetNodeByID(in.GetNodeId()) + if err != nil { + rpcErr := status.Errorf(codes.NotFound, "node %q not found", in.GetNodeId()) + recordRPCError(ctx, rpcErr) + return nil, rpcErr + } + return &controlv1.GetNodeResponse{Node: nodeToView(node)}, nil +} + +func (s *Service) ListWorkloads(ctx context.Context, in *controlv1.ListWorkloadsRequest) (*controlv1.ListWorkloadsResponse, error) { + if in != nil { + annotateRPC(ctx, + attribute.String("scheduler.filter_node_id", strings.TrimSpace(in.GetNodeId())), + attribute.String("scheduler.filter_status", strings.TrimSpace(in.GetStatus())), + ) + } + workloads, err := s.sched.GetWorkloads() + if err != nil { + rpcErr := status.Error(codes.Internal, err.Error()) + recordRPCError(ctx, rpcErr) + return nil, rpcErr + } + + filterNodeID := strings.TrimSpace(in.GetNodeId()) + filterStatus := strings.ToLower(strings.TrimSpace(in.GetStatus())) + out := make([]*controlv1.WorkloadView, 0, len(workloads)) + for _, workload := range workloads { + if filterNodeID != "" && assignedNodeID(workload) != filterNodeID { + continue + } + if filterStatus != "" && strings.ToLower(strings.TrimSpace(workload.Status)) != filterStatus { + continue + } + out = append(out, workloadToView(workload)) + } + return &controlv1.ListWorkloadsResponse{Workloads: out}, nil +} + +func (s *Service) GetWorkload(ctx context.Context, in *controlv1.GetWorkloadRequest) (*controlv1.GetWorkloadResponse, error) { + if in != nil { + annotateRPC(ctx, attribute.String("scheduler.workload_id", strings.TrimSpace(in.GetWorkloadId()))) + } + if in == nil || strings.TrimSpace(in.GetWorkloadId()) == "" { + err := status.Error(codes.InvalidArgument, "workload_id is required") + recordRPCError(ctx, err) + return nil, err + } + workload, err := s.sched.GetWorkloadByID(in.GetWorkloadId()) + if err != nil { + rpcErr := status.Errorf(codes.NotFound, "workload %q not found", in.GetWorkloadId()) + recordRPCError(ctx, rpcErr) + return nil, rpcErr + } + return &controlv1.GetWorkloadResponse{Workload: workloadToView(workload)}, nil +} + +func (s *Service) GetClusterSummary(ctx context.Context, _ *controlv1.GetClusterSummaryRequest) (*controlv1.GetClusterSummaryResponse, error) { + nodes, err := s.sched.GetNodes() + if err != nil { + rpcErr := status.Error(codes.Internal, err.Error()) + recordRPCError(ctx, rpcErr) + return nil, rpcErr + } + workloads, err := s.sched.GetWorkloads() + if err != nil { + rpcErr := status.Error(codes.Internal, err.Error()) + recordRPCError(ctx, rpcErr) + return nil, rpcErr + } + + resp := &controlv1.GetClusterSummaryResponse{ + TotalNodes: int32(len(nodes)), + TotalWorkloads: int32(len(workloads)), + GeneratedAt: timestamppb.Now(), + } + + for _, node := range nodes { + switch strings.ToLower(strings.TrimSpace(node.Status)) { + case "ready": + resp.ReadyNodes++ + default: + resp.NotReadyNodes++ + } + } + + for _, workload := range workloads { + statusValue := strings.ToLower(strings.TrimSpace(workload.Status)) + desiredState := strings.ToLower(strings.TrimSpace(workload.DesiredState)) + switch statusValue { + case "running": + resp.RunningWorkloads++ + case "failed": + resp.FailedWorkloads++ + case "pending", "unknown", "updating": + resp.PendingWorkloads++ + } + if desiredState == "deleted" || statusValue == "deleted" { + resp.DeletedWorkloads++ + } + } + + return resp, nil +} + +func (s *Service) ControlStream(stream controlv1.AgentControl_ControlStreamServer) error { + err := status.Error(codes.Unimplemented, "ControlStream is not implemented yet") + recordRPCError(stream.Context(), err) + return err +} + +func nodeToView(node models.Node) *controlv1.NodeView { + return &controlv1.NodeView{ + NodeId: node.NodeID, + Status: node.Status, + StatusReason: node.StatusReason, + StatusUpdatedBy: node.StatusUpdatedBy, + StatusUpdatedAt: timestampPtr(node.StatusUpdatedAt), + LastHeartbeat: timestampPtr(node.LastHeartbeat), + GrpcEndpoint: node.AgentEndpoint, + TotalCpuCores: node.TotalCPU, + AvailableCpuCores: node.AvailableCPU, + TotalMemoryMb: node.TotalMemory, + AvailableMemoryMb: node.AvailableMemory, + SupportedWorkloadTypes: append([]string(nil), node.SupportedWorkloadTypes...), + Labels: copyStringMap(node.Labels), + } +} + +func workloadToView(workload models.Workload) *controlv1.WorkloadView { + return &controlv1.WorkloadView{ + WorkloadId: workload.ID, + Type: workload.Type, + DesiredState: workload.DesiredState, + Status: workload.Status, + AssignedNodeId: assignedNodeID(workload), + RevisionId: workload.RevisionID, + RetryAttempts: int32(workload.Retry.Attempts), + RetryMaxAttempts: int32(workload.Retry.MaxAttempts), + RetryNextAt: timestampPtr(workload.Retry.NextRetryAt), + FailureReason: workload.StatusInfo.FailureReason, + LastUpdated: workloadLastUpdated(workload), + } +} + +func assignedNodeID(workload models.Workload) string { + if strings.TrimSpace(workload.NodeID) != "" { + return workload.NodeID + } + return workload.AssignedNode +} + +func workloadLastUpdated(workload models.Workload) *timestamppb.Timestamp { + if !workload.StatusInfo.LastUpdated.IsZero() { + return timestamppb.New(workload.StatusInfo.LastUpdated.UTC()) + } + if !workload.CreatedAt.IsZero() { + return timestamppb.New(workload.CreatedAt.UTC()) + } + return nil +} + +func timestampPtr(t time.Time) *timestamppb.Timestamp { + if t.IsZero() { + return nil + } + return timestamppb.New(t.UTC()) +} + +func copyStringMap(in map[string]string) map[string]string { + if len(in) == 0 { + return nil + } + out := make(map[string]string, len(in)) + for k, v := range in { + out[k] = v + } + return out +} + +func splitHostPort(endpoint string) (string, int, error) { + hostPort := strings.TrimSpace(endpoint) + parts := strings.Split(hostPort, ":") + if len(parts) < 2 { + return "", 0, fmt.Errorf("invalid grpc endpoint %q", endpoint) + } + port, err := strconv.Atoi(parts[len(parts)-1]) + if err != nil { + return "", 0, err + } + host := strings.Join(parts[:len(parts)-1], ":") + host = strings.Trim(host, "[]") + if host == "" { + host = "127.0.0.1" + } + return host, port, nil +} + +func maxInt64(a, b int64) int64 { + if a > b { + return a + } + return b +} + +func controlApplyToModel(in *controlv1.ApplyWorkloadRequest) (models.Workload, error) { + if in.GetSpec() == nil { + return models.Workload{}, fmt.Errorf("spec is required") + } + + w := models.Workload{ + ID: in.GetWorkloadId(), + Type: strings.ToLower(strings.TrimSpace(in.GetSpec().GetType())), + RevisionID: in.GetRevisionId(), + DesiredState: normalizeDesiredStateString(in.GetDesiredState()), + Metadata: map[string]interface{}{}, + EnvVars: map[string]string{}, + Labels: map[string]string{}, + } + if w.DesiredState == "" { + w.DesiredState = "Running" + } + + if r := in.GetSpec().GetResources(); r != nil { + w.Resources = models.Resources{ + CPUUsage: float64(r.GetCpuMillicores()) / 1000.0, + MemoryUsage: float64(r.GetMemoryMb()), + DiskUsage: int(r.GetDiskGb()), + } + } + + for k, v := range in.GetSpec().GetMetadata() { + w.Metadata[k] = v + } + + switch strings.ToLower(in.GetSpec().GetType()) { + case "container": + cs := in.GetSpec().GetContainer() + if cs == nil { + return models.Workload{}, fmt.Errorf("container spec required") + } + w.Image = cs.GetImage() + w.Command = strings.Join(cs.GetCommand(), " ") + w.EnvVars = cs.GetEnv() + w.RestartPolicy = cs.GetRestartPolicy() + for _, p := range cs.GetPorts() { + proto := p.GetProtocol() + if proto == "" { + proto = "tcp" + } + w.Ports = append(w.Ports, fmt.Sprintf("%d:%d/%s", p.GetHostPort(), p.GetContainerPort(), proto)) + } + for _, v := range cs.GetVolumes() { + vol := fmt.Sprintf("%s:%s", v.GetHostPath(), v.GetContainerPath()) + if v.GetReadOnly() { + vol += ":ro" + } + w.Volumes = append(w.Volumes, vol) + } + case "compose": + cp := in.GetSpec().GetCompose() + if cp == nil { + return models.Workload{}, fmt.Errorf("compose spec required") + } + w.Type = "compose" + w.EnvVars = cp.GetEnv() + if strings.EqualFold(cp.GetSourceType(), "git") { + w.GitRepo = cp.GetGitRepo() + w.GitBranch = cp.GetGitRef() + } else { + w.ComposeYAML = cp.GetInlineYaml() + } + case "vm": + vm := in.GetSpec().GetVm() + if vm == nil { + return models.Workload{}, fmt.Errorf("vm spec required") + } + w.VM = &models.VMSpec{ + VCPUs: vm.GetVcpus(), + MemoryMB: vm.GetMemoryMb(), + CloudInit: "", + } + if ci := vm.GetCloudInit(); ci != nil { + w.VM.CloudInitConfig = &models.CloudInitConfig{ + UserData: ci.GetUserData(), + MetaData: ci.GetMetaData(), + NetworkConfig: ci.GetNetworkConfig(), + } + } + for _, d := range vm.GetDisks() { + w.VM.Disks = append(w.VM.Disks, models.VMDiskConfig{Type: d.GetPoolName(), SizeGB: d.GetSizeGb(), Device: d.GetMountPoint()}) + } + for _, n := range vm.GetNetworks() { + w.VM.Networks = append(w.VM.Networks, models.VMNetworkConfig{Network: n.GetBridge(), IPAddress: n.GetStaticIp()}) + } + default: + return models.Workload{}, fmt.Errorf("unsupported workload type %q", in.GetSpec().GetType()) + } + + return w, nil +} + +func normalizeDesiredStateString(v string) string { + switch strings.ToLower(strings.TrimSpace(v)) { + case "stopped", "stop": + return "Stopped" + case "deleted", "delete": + return "Deleted" + default: + return "Running" + } +} + +func normalizeSupportedWorkloadTypes(types []string) []string { + if len(types) == 0 { + return nil + } + out := make([]string, 0, len(types)) + seen := make(map[string]struct{}, len(types)) + for _, t := range types { + canon := strings.ToLower(strings.TrimSpace(t)) + switch canon { + case "docker-container": + canon = "container" + case "docker-compose": + canon = "compose" + } + if canon == "" { + continue + } + if _, ok := seen[canon]; ok { + continue + } + seen[canon] = struct{}{} + out = append(out, canon) + } + return out +} diff --git a/persys-scheduler/internal/logging/logger.go b/persys-scheduler/internal/logging/logger.go new file mode 100644 index 0000000..be29153 --- /dev/null +++ b/persys-scheduler/internal/logging/logger.go @@ -0,0 +1,69 @@ +package logging + +import ( + "os" + "strings" + "sync" + + "github.com/sirupsen/logrus" +) + +var ( + baseLogger *logrus.Logger + initOnce sync.Once +) + +func Init() *logrus.Logger { + initOnce.Do(func() { + l := logrus.New() + l.SetOutput(os.Stdout) + + switch strings.ToLower(strings.TrimSpace(os.Getenv("LOG_FORMAT"))) { + case "", "text": + l.SetFormatter(&logrus.TextFormatter{ + FullTimestamp: true, + TimestampFormat: "2006-01-02T15:04:05-07:00", + ForceColors: true, + DisableColors: false, + PadLevelText: true, + DisableLevelTruncation: true, + }) + case "json": + l.SetFormatter(&logrus.JSONFormatter{}) + default: + l.SetFormatter(&logrus.TextFormatter{ + FullTimestamp: true, + TimestampFormat: "2006-01-02T15:04:05-07:00", + ForceColors: true, + DisableColors: false, + PadLevelText: true, + DisableLevelTruncation: true, + }) + } + + level := strings.ToLower(strings.TrimSpace(os.Getenv("LOG_LEVEL"))) + if level == "" { + level = "info" + } + parsedLevel, err := logrus.ParseLevel(level) + if err != nil { + parsedLevel = logrus.InfoLevel + } + l.SetLevel(parsedLevel) + + baseLogger = l + }) + + return baseLogger +} + +func L() *logrus.Logger { + if baseLogger == nil { + return Init() + } + return baseLogger +} + +func C(component string) *logrus.Entry { + return L().WithField("component", component) +} diff --git a/persys-scheduler/internal/metrics/metrics.go b/persys-scheduler/internal/metrics/metrics.go new file mode 100644 index 0000000..61fd783 --- /dev/null +++ b/persys-scheduler/internal/metrics/metrics.go @@ -0,0 +1,220 @@ +package metrics + +import ( + "context" + "sync" + "time" + + "github.com/prometheus/client_golang/prometheus" + "google.golang.org/grpc" + "google.golang.org/grpc/codes" + "google.golang.org/grpc/status" +) + +var ( + registerOnce sync.Once + + grpcServerRequestsTotal = prometheus.NewCounterVec( + prometheus.CounterOpts{ + Namespace: "persys", + Subsystem: "scheduler", + Name: "grpc_server_requests_total", + Help: "Total number of inbound gRPC requests to scheduler.", + }, + []string{"method", "code"}, + ) + grpcServerRequestDuration = prometheus.NewHistogramVec( + prometheus.HistogramOpts{ + Namespace: "persys", + Subsystem: "scheduler", + Name: "grpc_server_request_duration_seconds", + Help: "Inbound gRPC request latency for scheduler.", + Buckets: prometheus.DefBuckets, + }, + []string{"method", "code"}, + ) + + agentRPCRequestsTotal = prometheus.NewCounterVec( + prometheus.CounterOpts{ + Namespace: "persys", + Subsystem: "scheduler", + Name: "agent_rpc_requests_total", + Help: "Total number of outbound RPC calls from scheduler to agents.", + }, + []string{"rpc", "code"}, + ) + agentRPCDuration = prometheus.NewHistogramVec( + prometheus.HistogramOpts{ + Namespace: "persys", + Subsystem: "scheduler", + Name: "agent_rpc_duration_seconds", + Help: "Latency of outbound RPC calls from scheduler to agents.", + Buckets: prometheus.DefBuckets, + }, + []string{"rpc", "code"}, + ) + + reconciliationResultsTotal = prometheus.NewCounterVec( + prometheus.CounterOpts{ + Namespace: "persys", + Subsystem: "scheduler", + Name: "reconciliation_results_total", + Help: "Count of reconciliation results grouped by action and success.", + }, + []string{"action", "success"}, + ) + reconciliationCycleDuration = prometheus.NewHistogram( + prometheus.HistogramOpts{ + Namespace: "persys", + Subsystem: "scheduler", + Name: "reconciliation_cycle_duration_seconds", + Help: "Duration of full reconciliation cycles.", + Buckets: prometheus.DefBuckets, + }, + ) + reconciliationCyclesTotal = prometheus.NewCounterVec( + prometheus.CounterOpts{ + Namespace: "persys", + Subsystem: "scheduler", + Name: "reconciliation_cycles_total", + Help: "Total number of reconciliation cycles.", + }, + []string{"result"}, + ) + + nodeStatusGauge = prometheus.NewGaugeVec( + prometheus.GaugeOpts{ + Namespace: "persys", + Subsystem: "scheduler", + Name: "nodes_status", + Help: "Current number of nodes grouped by status.", + }, + []string{"status"}, + ) + workloadStatusGauge = prometheus.NewGaugeVec( + prometheus.GaugeOpts{ + Namespace: "persys", + Subsystem: "scheduler", + Name: "workloads_status", + Help: "Current number of workloads grouped by status.", + }, + []string{"status"}, + ) + workloadDesiredGauge = prometheus.NewGaugeVec( + prometheus.GaugeOpts{ + Namespace: "persys", + Subsystem: "scheduler", + Name: "workloads_desired_state", + Help: "Current number of workloads grouped by desired state.", + }, + []string{"desired_state"}, + ) +) + +var defaultNodeStatuses = []string{"ready", "active", "notready", "unknown"} +var defaultWorkloadStatuses = []string{"pending", "running", "stopped", "failed", "deleted", "updating", "unknown"} +var defaultDesiredStates = []string{"running", "stopped", "deleted", "unknown"} + +func init() { + Register() +} + +func Register() { + registerOnce.Do(func() { + prometheus.MustRegister( + grpcServerRequestsTotal, + grpcServerRequestDuration, + agentRPCRequestsTotal, + agentRPCDuration, + reconciliationResultsTotal, + reconciliationCycleDuration, + reconciliationCyclesTotal, + nodeStatusGauge, + workloadStatusGauge, + workloadDesiredGauge, + ) + + for _, s := range defaultNodeStatuses { + nodeStatusGauge.WithLabelValues(s).Set(0) + } + for _, s := range defaultWorkloadStatuses { + workloadStatusGauge.WithLabelValues(s).Set(0) + } + for _, s := range defaultDesiredStates { + workloadDesiredGauge.WithLabelValues(s).Set(0) + } + }) +} + +func GRPCUnaryServerInterceptor() grpc.UnaryServerInterceptor { + return func(ctx context.Context, req interface{}, info *grpc.UnaryServerInfo, handler grpc.UnaryHandler) (interface{}, error) { + start := time.Now() + resp, err := handler(ctx, req) + code := status.Code(err).String() + grpcServerRequestsTotal.WithLabelValues(info.FullMethod, code).Inc() + grpcServerRequestDuration.WithLabelValues(info.FullMethod, code).Observe(time.Since(start).Seconds()) + return resp, err + } +} + +func GRPCStreamServerInterceptor() grpc.StreamServerInterceptor { + return func(srv interface{}, ss grpc.ServerStream, info *grpc.StreamServerInfo, handler grpc.StreamHandler) error { + start := time.Now() + err := handler(srv, ss) + code := status.Code(err).String() + grpcServerRequestsTotal.WithLabelValues(info.FullMethod, code).Inc() + grpcServerRequestDuration.WithLabelValues(info.FullMethod, code).Observe(time.Since(start).Seconds()) + return err + } +} + +func ObserveAgentRPC(rpc string, err error, duration time.Duration) { + code := status.Code(err).String() + if err == nil { + code = codes.OK.String() + } + agentRPCRequestsTotal.WithLabelValues(rpc, code).Inc() + agentRPCDuration.WithLabelValues(rpc, code).Observe(duration.Seconds()) +} + +func ObserveReconciliationResult(action string, success bool) { + successLabel := "false" + if success { + successLabel = "true" + } + reconciliationResultsTotal.WithLabelValues(action, successLabel).Inc() +} + +func ObserveReconciliationCycle(duration time.Duration, err error) { + result := "success" + if err != nil { + result = "error" + } + reconciliationCyclesTotal.WithLabelValues(result).Inc() + reconciliationCycleDuration.Observe(duration.Seconds()) +} + +func SetNodeStatusCounts(counts map[string]int) { + for _, s := range defaultNodeStatuses { + nodeStatusGauge.WithLabelValues(s).Set(0) + } + for status, count := range counts { + nodeStatusGauge.WithLabelValues(status).Set(float64(count)) + } +} + +func SetWorkloadStatusCounts(statusCounts map[string]int, desiredCounts map[string]int) { + for _, s := range defaultWorkloadStatuses { + workloadStatusGauge.WithLabelValues(s).Set(0) + } + for status, count := range statusCounts { + workloadStatusGauge.WithLabelValues(status).Set(float64(count)) + } + + for _, s := range defaultDesiredStates { + workloadDesiredGauge.WithLabelValues(s).Set(0) + } + for desired, count := range desiredCounts { + workloadDesiredGauge.WithLabelValues(desired).Set(float64(count)) + } +} diff --git a/persys-scheduler/internal/models/models.go b/persys-scheduler/internal/models/models.go new file mode 100644 index 0000000..b6a3800 --- /dev/null +++ b/persys-scheduler/internal/models/models.go @@ -0,0 +1,188 @@ +package models + +import ( + "net/http" + "sync" + "time" + + clientv3 "go.etcd.io/etcd/client/v3" +) + +// Hypervisor represents virtualization technology details +type Hypervisor struct { + Type string `json:"type" binding:"required"` + Status string `json:"status" binding:"required"` + Version string `json:"version,omitempty"` +} + +// ContainerEngine represents container runtime details +type ContainerEngine struct { + Type string `json:"type" binding:"required"` + Status string `json:"status" binding:"required"` + Version string `json:"version,omitempty"` +} + +// Resources represents system resource usage +type Resources struct { + CPUUsage float64 `json:"cpu_usage,omitempty"` + MemoryUsage float64 `json:"memory_usage,omitempty"` + DiskUsage int `json:"disk_usage,omitempty"` +} + +// DockerSwarm represents swarm-specific information +type DockerSwarm struct { + Active bool `json:"active"` + NodeID string `json:"nodeId,omitempty"` + Role string `json:"role,omitempty"` + Status string `json:"status,omitempty"` + ManagerAddress string `json:"managerAddress,omitempty"` +} + +// Node represents a registered node +type Node struct { + NodeID string `json:"nodeId" binding:"required"` + IPAddress string `json:"ipAddress" binding:"required"` + Username string `json:"username" binding:"required"` + Hostname string `json:"hostname" binding:"required"` + OSName string `json:"osName" binding:"required"` + KernelVersion string `json:"kernelVersion" binding:"required"` + Status string `json:"status" binding:"required"` + Timestamp string `json:"timestamp" binding:"required"` + Resources Resources `json:"resources"` + TotalCPU float64 `json:"totalCpu"` + TotalMemory int64 `json:"totalMemory"` + AvailableCPU float64 `json:"availableCpu"` + AvailableMemory int64 `json:"availableMemory"` + Hypervisor Hypervisor `json:"hypervisor" binding:"required"` + ContainerEngine ContainerEngine `json:"containerEngine" binding:"required"` + DockerSwarm DockerSwarm `json:"dockerSwarm"` + LastHeartbeat time.Time `json:"lastHeartbeat"` + StatusReason string `json:"statusReason,omitempty"` + StatusUpdatedBy string `json:"statusUpdatedBy,omitempty"` + StatusUpdatedAt time.Time `json:"statusUpdatedAt,omitempty"` + Labels map[string]string `json:"labels,omitempty"` + AgentPort int `json:"agentPort"` // Added for agent communication + AgentGRPCPort int `json:"agentGrpcPort,omitempty"` + AgentEndpoint string `json:"agentEndpoint,omitempty"` + SupportedWorkloadTypes []string `json:"supportedWorkloadTypes,omitempty"` + DomainName string `json:"domainName,omitempty"` // Added field +} + +// Workload represents a scheduled task +type Workload struct { + ID string `json:"id,omitempty"` + Name string `json:"name" binding:"required"` + Type string `json:"type" binding:"required"` // "docker-container", "docker-compose", "compose", "vm" + RevisionID string `json:"revisionId,omitempty"` // stable revision for idempotent apply + AssignedNode string `json:"assignedNode,omitempty"` + Image string `json:"image,omitempty"` // For docker-container + Command string `json:"command,omitempty"` // For docker-container + Compose string `json:"compose,omitempty"` // Base64-encoded Compose content (optional) + ComposeYAML string `json:"composeYaml,omitempty"` // Base64-encoded Compose YAML for compute-agent compose spec + ProjectName string `json:"projectName,omitempty"` // Deterministic compose project name + GitRepo string `json:"gitRepo,omitempty"` // Git URL for git-compose + GitBranch string `json:"gitBranch,omitempty"` // Git branch for git-compose + GitToken string `json:"gitToken,omitempty"` // Optional Git auth token + EnvVars map[string]string `json:"envVars,omitempty"` // Environment variables + Resources Resources `json:"resources"` + NodeID string `json:"nodeId,omitempty"` + Status string `json:"status"` + DesiredState string `json:"desiredState,omitempty"` // Desired state for reconciliation + Labels map[string]string `json:"labels,omitempty"` + CreatedAt time.Time `json:"createdAt"` + LocalPath string `json:"localPath,omitempty"` // Local path for docker-compose + Ports []string `json:"ports,omitempty"` // e.g., ["8080:80"] + Volumes []string `json:"volumes,omitempty"` // e.g., ["/host:/container"] + Network string `json:"network,omitempty"` // e.g., "bridge" + RestartPolicy string `json:"restartPolicy,omitempty"` // e.g., "always" + Logs string `json:"logs,omitempty"` // Execution logs and output + Metadata map[string]interface{} `json:"metadata,omitempty"` // Reconciliation metadata + Retry RetryState `json:"retry"` + StatusInfo WorkloadStatusInfo `json:"statusInfo"` + VM *VMSpec `json:"vm,omitempty"` // VM workload spec +} + +type RetryState struct { + Attempts int `json:"attempts"` + MaxAttempts int `json:"maxAttempts"` + NextRetryAt time.Time `json:"nextRetryAt,omitempty"` +} + +type WorkloadStatusInfo struct { + ActualState string `json:"actualState,omitempty"` + LastUpdated time.Time `json:"lastUpdated,omitempty"` + FailureReason string `json:"failureReason,omitempty"` +} + +type AssignmentRecord struct { + WorkloadID string `json:"workloadId"` + NodeID string `json:"nodeId"` + Reason string `json:"reason"` + CreatedAt time.Time `json:"createdAt"` +} + +type ReconciliationRecord struct { + WorkloadID string `json:"workloadId"` + Action string `json:"action"` + Success bool `json:"success"` + Reason string `json:"reason,omitempty"` + AttemptedAt time.Time `json:"attemptedAt"` +} + +type SchedulerEvent struct { + ID string `json:"id"` + Type string `json:"type"` + WorkloadID string `json:"workloadId,omitempty"` + NodeID string `json:"nodeId,omitempty"` + Reason string `json:"reason,omitempty"` + Timestamp time.Time `json:"timestamp"` + Details map[string]interface{} `json:"details,omitempty"` +} + +// VMSpec defines VM-specific fields for scheduler API and persistence. +type VMSpec struct { + Name string `json:"name,omitempty"` + VCPUs int32 `json:"vcpus,omitempty"` + MemoryMB int64 `json:"memoryMb,omitempty"` + Disks []VMDiskConfig `json:"disks,omitempty"` + Networks []VMNetworkConfig `json:"networks,omitempty"` + CloudInit string `json:"cloudInit,omitempty"` + Metadata map[string]string `json:"metadata,omitempty"` + CloudInitConfig *CloudInitConfig `json:"cloudInitConfig,omitempty"` +} + +type VMDiskConfig struct { + Path string `json:"path,omitempty"` + Device string `json:"device,omitempty"` + Format string `json:"format,omitempty"` + SizeGB int64 `json:"sizeGb,omitempty"` + Type string `json:"type,omitempty"` + Boot bool `json:"boot,omitempty"` +} + +type VMNetworkConfig struct { + Network string `json:"network,omitempty"` + MAC string `json:"macAddress,omitempty"` + IPAddress string `json:"ipAddress,omitempty"` +} + +type CloudInitConfig struct { + UserData string `json:"userData,omitempty"` + MetaData string `json:"metaData,omitempty"` + NetworkConfig string `json:"networkConfig,omitempty"` + VendorData string `json:"vendorData,omitempty"` +} + +// AgentCommand represents a command payload for the agent API +type AgentCommand struct { + Command string `json:"command"` + Image string `json:"image,omitempty"` +} + +// Scheduler manages nodes and workloads +type Scheduler struct { + etcdClient *clientv3.Client + mu sync.Mutex + httpClient *http.Client + domain string // Added domain field for DNS +} diff --git a/persys-scheduler/internal/scheduler/agent_grpc.go b/persys-scheduler/internal/scheduler/agent_grpc.go new file mode 100644 index 0000000..36bf1e3 --- /dev/null +++ b/persys-scheduler/internal/scheduler/agent_grpc.go @@ -0,0 +1,522 @@ +package scheduler + +import ( + "context" + "crypto/sha256" + "crypto/tls" + "crypto/x509" + "encoding/hex" + "encoding/json" + "fmt" + "os" + "strconv" + "strings" + "time" + + agentpb "github.com/persys-dev/persys-cloud/persys-scheduler/internal/agentpb" + "github.com/persys-dev/persys-cloud/persys-scheduler/internal/logging" + metricspkg "github.com/persys-dev/persys-cloud/persys-scheduler/internal/metrics" + "github.com/persys-dev/persys-cloud/persys-scheduler/internal/models" + "github.com/sirupsen/logrus" + "go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc" + "google.golang.org/grpc" + "google.golang.org/grpc/credentials" + "google.golang.org/grpc/credentials/insecure" + "google.golang.org/grpc/status" +) + +const ( + defaultPollInterval = time.Second + defaultApplyTimeout = 45 * time.Second + defaultVMApplyTimeout = 240 * time.Second + defaultDeleteTimeout = 60 * time.Second + defaultRPCTimeout = 10 * time.Second +) + +var agentLogger = logging.C("scheduler.agent_grpc") + +func schedulerDurationEnv(key string, fallback time.Duration) time.Duration { + raw := strings.TrimSpace(os.Getenv(key)) + if raw == "" { + return fallback + } + d, err := time.ParseDuration(raw) + if err == nil { + return d + } + if n, err := strconv.Atoi(raw); err == nil { + return time.Duration(n) * time.Second + } + agentLogger.WithFields(logrus.Fields{ + "key": key, + "value": raw, + "fallback": fallback.String(), + }).Warn("invalid duration env value; using fallback") + return fallback +} + +func (s *Scheduler) agentPollInterval() time.Duration { + return schedulerDurationEnv("SCHEDULER_AGENT_STATUS_POLL_INTERVAL", defaultPollInterval) +} + +func (s *Scheduler) applyTimeoutFor(workload models.Workload) time.Duration { + if strings.EqualFold(workload.Type, "vm") { + return schedulerDurationEnv("SCHEDULER_AGENT_VM_APPLY_TIMEOUT", defaultVMApplyTimeout) + } + return schedulerDurationEnv("SCHEDULER_AGENT_APPLY_TIMEOUT", defaultApplyTimeout) +} + +func (s *Scheduler) deleteTimeout() time.Duration { + return schedulerDurationEnv("SCHEDULER_AGENT_DELETE_TIMEOUT", defaultDeleteTimeout) +} + +func (s *Scheduler) rpcTimeout() time.Duration { + return schedulerDurationEnv("SCHEDULER_AGENT_RPC_TIMEOUT", defaultRPCTimeout) +} + +func (s *Scheduler) grpcAddressForNode(node models.Node) string { + if endpoint := strings.TrimSpace(node.AgentEndpoint); endpoint != "" { + return endpoint + } + port := node.AgentPort + if node.AgentGRPCPort > 0 { + port = node.AgentGRPCPort + } + return fmt.Sprintf("%s:%d", node.IPAddress, port) +} + +func loadClientTLSConfigFromEnv() (*tls.Config, error) { + caPath := strings.TrimSpace(os.Getenv("PERSYS_TLS_CA")) + certPath := strings.TrimSpace(os.Getenv("PERSYS_TLS_CERT")) + keyPath := strings.TrimSpace(os.Getenv("PERSYS_TLS_KEY")) + + if caPath == "" { + return nil, fmt.Errorf("PERSYS_TLS_CA must be set when PERSYS_TLS_ENABLED=true") + } + caPEM, err := os.ReadFile(caPath) + if err != nil { + return nil, fmt.Errorf("read CA file %s: %w", caPath, err) + } + caPool := x509.NewCertPool() + if !caPool.AppendCertsFromPEM(caPEM) { + return nil, fmt.Errorf("invalid CA PEM in %s", caPath) + } + + tlsCfg := &tls.Config{MinVersion: tls.VersionTLS12, RootCAs: caPool} + if certPath != "" && keyPath != "" { + cert, err := tls.LoadX509KeyPair(certPath, keyPath) + if err != nil { + return nil, fmt.Errorf("load client key pair: %w", err) + } + tlsCfg.Certificates = []tls.Certificate{cert} + } + return tlsCfg, nil +} + +func (s *Scheduler) newAgentClient(node models.Node) (agentpb.AgentServiceClient, *grpc.ClientConn, error) { + addr := s.grpcAddressForNode(node) + if strings.TrimSpace(addr) == "" { + return nil, nil, fmt.Errorf("node %s has invalid grpc address", node.NodeID) + } + + var dialOpts []grpc.DialOption + if strings.EqualFold(os.Getenv("PERSYS_TLS_ENABLED"), "true") { + tlsCfg, err := loadClientTLSConfigFromEnv() + if err != nil { + return nil, nil, err + } + dialOpts = append(dialOpts, grpc.WithTransportCredentials(credentials.NewTLS(tlsCfg))) + } else { + dialOpts = append(dialOpts, grpc.WithTransportCredentials(insecure.NewCredentials())) + } + dialOpts = append(dialOpts, grpc.WithStatsHandler(otelgrpc.NewClientHandler())) + + ctx, cancel := context.WithTimeout(context.Background(), s.rpcTimeout()) + defer cancel() + conn, err := grpc.DialContext(ctx, addr, dialOpts...) + if err != nil { + return nil, nil, fmt.Errorf("dial agent %s (%s): %w", node.NodeID, addr, err) + } + return agentpb.NewAgentServiceClient(conn), conn, nil +} + +func normalizeDesiredState(v string) agentpb.DesiredState { + switch strings.ToLower(strings.TrimSpace(v)) { + case "", "running": + return agentpb.DesiredState_DESIRED_STATE_RUNNING + case "stopped", "stop": + return agentpb.DesiredState_DESIRED_STATE_STOPPED + default: + return agentpb.DesiredState_DESIRED_STATE_RUNNING + } +} + +func (s *Scheduler) ensureWorkloadRevision(workload *models.Workload) { + if strings.TrimSpace(workload.RevisionID) != "" { + return + } + clone := *workload + clone.CreatedAt = time.Time{} + clone.Logs = "" + clone.Status = "" + clone.Metadata = nil + payload, _ := json.Marshal(clone) + h := sha256.Sum256(payload) + workload.RevisionID = hex.EncodeToString(h[:12]) +} + +func parsePortMapping(port string) (*agentpb.PortMapping, error) { + if strings.TrimSpace(port) == "" { + return nil, fmt.Errorf("empty port mapping") + } + parts := strings.Split(port, ":") + if len(parts) != 2 { + return nil, fmt.Errorf("invalid port mapping %q", port) + } + hostPortRaw := parts[0] + containerSpec := parts[1] + protocol := "tcp" + if strings.Contains(containerSpec, "/") { + specParts := strings.SplitN(containerSpec, "/", 2) + containerSpec = specParts[0] + if specParts[1] != "" { + protocol = strings.ToLower(specParts[1]) + } + } + hostPort, err := strconv.Atoi(hostPortRaw) + if err != nil { + return nil, fmt.Errorf("invalid host port in %q: %w", port, err) + } + containerPort, err := strconv.Atoi(containerSpec) + if err != nil { + return nil, fmt.Errorf("invalid container port in %q: %w", port, err) + } + return &agentpb.PortMapping{HostPort: int32(hostPort), ContainerPort: int32(containerPort), Protocol: protocol}, nil +} + +func parseVolumeMount(volume string) (*agentpb.VolumeMount, error) { + if strings.TrimSpace(volume) == "" { + return nil, fmt.Errorf("empty volume mapping") + } + parts := strings.Split(volume, ":") + if len(parts) < 2 { + return nil, fmt.Errorf("invalid volume mapping %q", volume) + } + readOnly := false + if len(parts) > 2 { + readOnly = strings.EqualFold(parts[2], "ro") + } + return &agentpb.VolumeMount{HostPath: parts[0], ContainerPath: parts[1], ReadOnly: readOnly}, nil +} + +func normalizeProjectName(name string) string { + name = strings.TrimSpace(strings.ToLower(name)) + if name == "" { + return "workload" + } + var b strings.Builder + for _, r := range name { + if (r >= 'a' && r <= 'z') || (r >= '0' && r <= '9') || r == '-' || r == '_' { + b.WriteRune(r) + } else { + b.WriteRune('-') + } + } + out := strings.Trim(b.String(), "-") + if out == "" { + return "workload" + } + return out +} + +func (s *Scheduler) buildApplyWorkloadRequest(workload models.Workload) (*agentpb.ApplyWorkloadRequest, error) { + s.ensureWorkloadRevision(&workload) + + req := &agentpb.ApplyWorkloadRequest{ + Id: workload.ID, + RevisionId: workload.RevisionID, + DesiredState: normalizeDesiredState(workload.DesiredState), + Spec: &agentpb.WorkloadSpec{}, + } + + switch strings.ToLower(strings.TrimSpace(workload.Type)) { + case "docker-container", "container": + req.Type = agentpb.WorkloadType_WORKLOAD_TYPE_CONTAINER + containerSpec := &agentpb.ContainerSpec{ + Image: workload.Image, + Env: workload.EnvVars, + Labels: workload.Labels, + RestartPolicy: &agentpb.RestartPolicy{ + Policy: workload.RestartPolicy, + }, + } + if cmd := strings.TrimSpace(workload.Command); cmd != "" { + containerSpec.Command = strings.Fields(cmd) + } + for _, port := range workload.Ports { + pm, err := parsePortMapping(port) + if err != nil { + return nil, err + } + containerSpec.Ports = append(containerSpec.Ports, pm) + } + for _, volume := range workload.Volumes { + vm, err := parseVolumeMount(volume) + if err != nil { + return nil, err + } + containerSpec.Volumes = append(containerSpec.Volumes, vm) + } + req.Spec.Spec = &agentpb.WorkloadSpec_Container{Container: containerSpec} + + case "docker-compose", "compose": + req.Type = agentpb.WorkloadType_WORKLOAD_TYPE_COMPOSE + composeYAML := strings.TrimSpace(workload.ComposeYAML) + if composeYAML == "" { + composeYAML = strings.TrimSpace(workload.Compose) + } + if composeYAML == "" { + return nil, fmt.Errorf("compose_yaml is required for compose workloads") + } + projectName := strings.TrimSpace(workload.ProjectName) + if projectName == "" { + projectName = normalizeProjectName(workload.Name) + } + req.Spec.Spec = &agentpb.WorkloadSpec_Compose{Compose: &agentpb.ComposeSpec{ + ProjectName: projectName, + ComposeYaml: composeYAML, + Env: workload.EnvVars, + }} + + case "vm": + req.Type = agentpb.WorkloadType_WORKLOAD_TYPE_VM + if workload.VM == nil { + return nil, fmt.Errorf("vm spec is required for vm workloads") + } + vmSpec := &agentpb.VMSpec{ + Name: workload.VM.Name, + Vcpus: workload.VM.VCPUs, + MemoryMb: workload.VM.MemoryMB, + CloudInit: workload.VM.CloudInit, + Metadata: workload.VM.Metadata, + } + if workload.VM.CloudInitConfig != nil { + vmSpec.CloudInitConfig = &agentpb.CloudInitConfig{ + UserData: workload.VM.CloudInitConfig.UserData, + MetaData: workload.VM.CloudInitConfig.MetaData, + NetworkConfig: workload.VM.CloudInitConfig.NetworkConfig, + VendorData: workload.VM.CloudInitConfig.VendorData, + } + } + for _, disk := range workload.VM.Disks { + vmSpec.Disks = append(vmSpec.Disks, &agentpb.DiskConfig{ + Path: disk.Path, + Device: disk.Device, + Format: disk.Format, + SizeGb: disk.SizeGB, + Type: disk.Type, + Boot: disk.Boot, + }) + } + for _, network := range workload.VM.Networks { + vmSpec.Networks = append(vmSpec.Networks, &agentpb.NetworkConfig{ + Network: network.Network, + MacAddress: network.MAC, + IpAddress: network.IPAddress, + }) + } + req.Spec.Spec = &agentpb.WorkloadSpec_Vm{Vm: vmSpec} + + default: + return nil, fmt.Errorf("unsupported workload type: %s", workload.Type) + } + + return req, nil +} + +func (s *Scheduler) applyWorkloadOnNode(ctx context.Context, node models.Node, workload models.Workload) (resp *agentpb.ApplyWorkloadResponse, err error) { + start := time.Now() + defer func() { + metricspkg.ObserveAgentRPC("ApplyWorkload", err, time.Since(start)) + }() + + client, conn, err := s.newAgentClient(node) + if err != nil { + return nil, err + } + defer conn.Close() + + req, err := s.buildApplyWorkloadRequest(workload) + if err != nil { + return nil, err + } + + if ctx == nil { + ctx = context.Background() + } + ctx, cancel := context.WithTimeout(ctx, s.rpcTimeout()) + defer cancel() + resp, err = client.ApplyWorkload(ctx, req) + if err != nil { + return nil, err + } + return resp, nil +} + +func (s *Scheduler) getWorkloadStatusFromNode(ctx context.Context, node models.Node, workloadID string) (resp *agentpb.WorkloadStatus, err error) { + start := time.Now() + defer func() { + metricspkg.ObserveAgentRPC("GetWorkloadStatus", err, time.Since(start)) + }() + + client, conn, err := s.newAgentClient(node) + if err != nil { + return nil, err + } + defer conn.Close() + + if ctx == nil { + ctx = context.Background() + } + ctx, cancel := context.WithTimeout(ctx, s.rpcTimeout()) + defer cancel() + statusResp, err := client.GetWorkloadStatus(ctx, &agentpb.GetWorkloadStatusRequest{Id: workloadID}) + if err != nil { + return nil, err + } + resp = statusResp.GetStatus() + return resp, nil +} + +func (s *Scheduler) getWorkloadActionsFromNode(ctx context.Context, node models.Node, workloadID string, limit int32) (actions []*agentpb.AgentAction, err error) { + start := time.Now() + defer func() { + metricspkg.ObserveAgentRPC("ListActions", err, time.Since(start)) + }() + + client, conn, err := s.newAgentClient(node) + if err != nil { + return nil, err + } + defer conn.Close() + + if ctx == nil { + ctx = context.Background() + } + ctx, cancel := context.WithTimeout(ctx, s.rpcTimeout()) + defer cancel() + resp, err := client.ListActions(ctx, &agentpb.ListActionsRequest{WorkloadId: workloadID, NewestFirst: true, Limit: limit}) + if err != nil { + return nil, err + } + actions = resp.GetActions() + return actions, nil +} + +func (s *Scheduler) deleteWorkloadFromNode(ctx context.Context, node models.Node, workloadID string) (resp *agentpb.DeleteWorkloadResponse, err error) { + start := time.Now() + defer func() { + metricspkg.ObserveAgentRPC("DeleteWorkload", err, time.Since(start)) + }() + + client, conn, err := s.newAgentClient(node) + if err != nil { + return nil, err + } + defer conn.Close() + + if ctx == nil { + ctx = context.Background() + } + ctx, cancel := context.WithTimeout(ctx, s.rpcTimeout()) + defer cancel() + resp, err = client.DeleteWorkload(ctx, &agentpb.DeleteWorkloadRequest{Id: workloadID}) + return resp, err +} + +func mapActualStateToSchedulerStatus(actual agentpb.ActualState) string { + switch actual { + case agentpb.ActualState_ACTUAL_STATE_PENDING: + return "Pending" + case agentpb.ActualState_ACTUAL_STATE_RUNNING: + return "Running" + case agentpb.ActualState_ACTUAL_STATE_STOPPED: + return "Stopped" + case agentpb.ActualState_ACTUAL_STATE_FAILED: + return "Failed" + case agentpb.ActualState_ACTUAL_STATE_UNKNOWN: + return "Unknown" + default: + return "Unknown" + } +} + +func isTerminalActualState(actual agentpb.ActualState) bool { + return actual == agentpb.ActualState_ACTUAL_STATE_RUNNING || actual == agentpb.ActualState_ACTUAL_STATE_STOPPED || actual == agentpb.ActualState_ACTUAL_STATE_FAILED +} + +func grpcStatusCode(err error) string { + st, ok := status.FromError(err) + if !ok { + return "" + } + return st.Code().String() +} + +func isWorkloadStatusNotFound(err error) bool { + if err == nil { + return false + } + if strings.EqualFold(grpcStatusCode(err), "not_found") { + return true + } + + msg := strings.ToLower(err.Error()) + return strings.Contains(msg, "status not found") || + strings.Contains(msg, "workload not found") || + strings.Contains(msg, "not found") +} + +func isNodeUnreachableError(err error) bool { + if err == nil { + return false + } + code := strings.ToLower(grpcStatusCode(err)) + if code == "unavailable" || code == "deadline_exceeded" { + return true + } + + msg := strings.ToLower(err.Error()) + return strings.Contains(msg, "connection refused") || + strings.Contains(msg, "no such host") || + strings.Contains(msg, "transport: error while dialing") || + strings.Contains(msg, "connection error") +} + +func (s *Scheduler) waitForWorkloadTerminalStatus(ctx context.Context, node models.Node, workload models.Workload, timeout time.Duration) (*agentpb.WorkloadStatus, []*agentpb.AgentAction, error) { + if ctx == nil { + ctx = context.Background() + } + deadline := time.Now().Add(timeout) + for { + statusResp, err := s.getWorkloadStatusFromNode(ctx, node, workload.ID) + if err == nil { + if statusResp != nil && isTerminalActualState(statusResp.GetActualState()) { + return statusResp, nil, nil + } + } else { + if isWorkloadStatusNotFound(err) { + return nil, nil, err + } + } + + if time.Now().After(deadline) { + actions, aErr := s.getWorkloadActionsFromNode(ctx, node, workload.ID, 20) + if aErr != nil { + return nil, nil, fmt.Errorf("timeout waiting for workload terminal state; also failed to query actions: %v", aErr) + } + return nil, actions, fmt.Errorf("timeout waiting for workload terminal state") + } + time.Sleep(s.agentPollInterval()) + } +} diff --git a/prow/internal/scheduler/dns.go b/persys-scheduler/internal/scheduler/dns.go similarity index 96% rename from prow/internal/scheduler/dns.go rename to persys-scheduler/internal/scheduler/dns.go index a58c0c3..b0e4c8e 100644 --- a/prow/internal/scheduler/dns.go +++ b/persys-scheduler/internal/scheduler/dns.go @@ -5,7 +5,7 @@ import ( "fmt" "strings" - "github.com/persys-dev/prow/internal/models" + "github.com/persys-dev/persys-cloud/persys-scheduler/internal/models" ) func (s *Scheduler) UpdateCoreDNS(node models.Node) error { diff --git a/prow/internal/scheduler/etcd.go b/persys-scheduler/internal/scheduler/etcd.go similarity index 73% rename from prow/internal/scheduler/etcd.go rename to persys-scheduler/internal/scheduler/etcd.go index 5b292de..6e30a56 100644 --- a/prow/internal/scheduler/etcd.go +++ b/persys-scheduler/internal/scheduler/etcd.go @@ -2,14 +2,15 @@ package scheduler import ( "context" - "time" "fmt" - "log" + "time" + "github.com/persys-dev/persys-cloud/persys-scheduler/internal/logging" + "github.com/sirupsen/logrus" "go.etcd.io/etcd/client/v3" ) - +var etcdLogger = logging.C("scheduler.etcd") // RetryableEtcdPut performs a put operation with retries. func (s *Scheduler) RetryableEtcdPut(key, value string) error { @@ -21,7 +22,10 @@ func (s *Scheduler) RetryableEtcdPut(key, value string) error { if err == nil { return nil } - log.Printf("Attempt %d failed to put key %s: %v", attempt+1, key, err) + etcdLogger.WithError(err).WithFields(logrus.Fields{ + "attempt": attempt + 1, + "key": key, + }).Warn("etcd put attempt failed") if attempt < maxRetries { time.Sleep(retryWaitTime) } @@ -39,7 +43,10 @@ func (s *Scheduler) RetryableEtcdGet(key string, opts ...clientv3.OpOption) (*cl if err == nil { return resp, nil } - log.Printf("Attempt %d failed to get key %s: %v", attempt+1, key, err) + etcdLogger.WithError(err).WithFields(logrus.Fields{ + "attempt": attempt + 1, + "key": key, + }).Warn("etcd get attempt failed") if attempt < maxRetries { time.Sleep(retryWaitTime) } diff --git a/persys-scheduler/internal/scheduler/metrics.go b/persys-scheduler/internal/scheduler/metrics.go new file mode 100644 index 0000000..9e9e3c7 --- /dev/null +++ b/persys-scheduler/internal/scheduler/metrics.go @@ -0,0 +1,43 @@ +package scheduler + +import ( + "strings" + + metricspkg "github.com/persys-dev/persys-cloud/persys-scheduler/internal/metrics" +) + +func normalizeLabel(v string) string { + v = strings.ToLower(strings.TrimSpace(v)) + if v == "" { + return "unknown" + } + return v +} + +// RefreshStateMetrics snapshots node/workload state into Prometheus gauges. +func (s *Scheduler) RefreshStateMetrics() error { + nodes, err := s.GetNodes() + if err != nil { + return err + } + workloads, err := s.GetWorkloads() + if err != nil { + return err + } + + nodeCounts := make(map[string]int) + for _, n := range nodes { + nodeCounts[normalizeLabel(n.Status)]++ + } + + workloadStatusCounts := make(map[string]int) + workloadDesiredCounts := make(map[string]int) + for _, w := range workloads { + workloadStatusCounts[normalizeLabel(w.Status)]++ + workloadDesiredCounts[normalizeLabel(w.DesiredState)]++ + } + + metricspkg.SetNodeStatusCounts(nodeCounts) + metricspkg.SetWorkloadStatusCounts(workloadStatusCounts, workloadDesiredCounts) + return nil +} diff --git a/persys-scheduler/internal/scheduler/monitor.go b/persys-scheduler/internal/scheduler/monitor.go new file mode 100644 index 0000000..31f402f --- /dev/null +++ b/persys-scheduler/internal/scheduler/monitor.go @@ -0,0 +1,104 @@ +package scheduler + +import ( + "context" + "fmt" + "strings" + "time" + + "github.com/persys-dev/persys-cloud/persys-scheduler/internal/logging" + "github.com/sirupsen/logrus" +) + +var monitorLogger = logging.C("scheduler.monitor") + +// Monitor handles workload status monitoring. +type Monitor struct { + scheduler *Scheduler +} + +// NewMonitor creates a new Monitor instance. +func NewMonitor(scheduler *Scheduler) *Monitor { + return &Monitor{scheduler: scheduler} +} + +func (m *Monitor) syncWorkloadStatus(workloadID string) error { + workload, err := m.scheduler.GetWorkloadByID(workloadID) + if err != nil { + return err + } + if workload.NodeID == "" { + return nil + } + + node, err := m.scheduler.GetNodeByID(workload.NodeID) + if err != nil { + return fmt.Errorf("resolve node %s: %w", workload.NodeID, err) + } + if !strings.EqualFold(node.Status, "active") { + return nil + } + + statusResp, err := m.scheduler.getWorkloadStatusFromNode(context.Background(), node, workload.ID) + if err != nil { + if isWorkloadStatusNotFound(err) { + if workload.Status != "Deleted" { + _ = m.scheduler.UpdateWorkloadStatus(workload.ID, "Deleted") + } + return nil + } + return fmt.Errorf("GetWorkloadStatus failed for workload %s on node %s: %w", workload.ID, node.NodeID, err) + } + if statusResp == nil { + return nil + } + + newStatus := mapActualStateToSchedulerStatus(statusResp.GetActualState()) + if newStatus != workload.Status { + if err := m.scheduler.UpdateWorkloadStatus(workload.ID, newStatus); err != nil { + return err + } + } + if msg := strings.TrimSpace(statusResp.GetMessage()); msg != "" { + _ = m.scheduler.UpdateWorkloadLogs(workload.ID, msg) + } + return nil +} + +// MonitorWorkloads periodically checks workload statuses and updates etcd. +func (m *Monitor) MonitorWorkloads(ctx context.Context, interval time.Duration) { + ticker := time.NewTicker(interval) + defer ticker.Stop() + + for { + select { + case <-ctx.Done(): + monitorLogger.Info("stopping workload monitoring") + return + case <-ticker.C: + workloads, err := m.scheduler.GetWorkloads() + if err != nil { + monitorLogger.WithError(err).Error("failed to get workloads for monitoring") + continue + } + for _, workload := range workloads { + if workload.Status == "Deleted" { + continue + } + if err := m.syncWorkloadStatus(workload.ID); err != nil { + monitorLogger.WithError(err).WithFields(logrus.Fields{ + "workload_id": workload.ID, + }).Warn("failed to sync workload") + } + } + if err := m.scheduler.RefreshStateMetrics(); err != nil { + monitorLogger.WithError(err).Warn("failed to refresh scheduler state metrics") + } + } + } +} + +// StartMonitoring initializes the workload monitoring routine using caller-managed context. +func (m *Monitor) StartMonitoring(ctx context.Context) { + go m.MonitorWorkloads(ctx, 60*time.Second) +} diff --git a/persys-scheduler/internal/scheduler/node_control.go b/persys-scheduler/internal/scheduler/node_control.go new file mode 100644 index 0000000..5875eb6 --- /dev/null +++ b/persys-scheduler/internal/scheduler/node_control.go @@ -0,0 +1,193 @@ +package scheduler + +import ( + "encoding/json" + "fmt" + "strings" + "time" + + "github.com/persys-dev/persys-cloud/persys-scheduler/internal/logging" + "github.com/persys-dev/persys-cloud/persys-scheduler/internal/models" + "github.com/sirupsen/logrus" +) + +var nodeLogger = logging.C("scheduler.node_control") + +func (s *Scheduler) UpdateNodeHeartbeat(nodeID, status string, availableCPU float64, availableMemory int64) error { + resp, err := s.RetryableEtcdGet("/nodes/" + nodeID) + if err != nil { + return fmt.Errorf("failed to get node %s from etcd: %w", nodeID, err) + } + if resp == nil || len(resp.Kvs) == 0 { + return fmt.Errorf("node %s not found", nodeID) + } + + var node models.Node + if err := json.Unmarshal(resp.Kvs[0].Value, &node); err != nil { + return fmt.Errorf("failed to unmarshal node %s: %w", nodeID, err) + } + + node.LastHeartbeat = time.Now().UTC() + if strings.TrimSpace(status) != "" { + previousStatus := node.Status + node.Status = status + if strings.EqualFold(status, "Ready") { + if strings.EqualFold(previousStatus, "Ready") { + node.StatusReason = "heartbeat received" + } else { + node.StatusReason = fmt.Sprintf("status transition %s -> %s via heartbeat", previousStatus, status) + nodeLogger.WithField("node_id", nodeID).Info("node recovered to Ready via heartbeat") + } + node.StatusUpdatedBy = "heartbeat" + node.StatusUpdatedAt = time.Now().UTC() + } else if !strings.EqualFold(previousStatus, status) { + node.StatusReason = "heartbeat status transition" + node.StatusUpdatedBy = "heartbeat" + node.StatusUpdatedAt = time.Now().UTC() + } + } + if availableCPU >= 0 { + node.AvailableCPU = availableCPU + } + if availableMemory >= 0 { + node.AvailableMemory = availableMemory + } + + updatedNodeJSON, err := json.Marshal(node) + if err != nil { + return fmt.Errorf("failed to marshal node %s: %w", nodeID, err) + } + if err := s.RetryableEtcdPut("/nodes/"+nodeID, string(updatedNodeJSON)); err != nil { + return fmt.Errorf("failed to update node %s heartbeat: %w", nodeID, err) + } + _ = s.RetryableEtcdPut("/nodes/"+nodeID+"/status", node.Status) + return nil +} + +func (s *Scheduler) MarkNodeNotReady(nodeID, reason string) error { + return s.markNodeNotReady(nodeID, reason, "reconciler") +} + +func (s *Scheduler) MarkNodeWorkloadTypeUnsupported(nodeID, workloadType, reason string) error { + resp, err := s.RetryableEtcdGet("/nodes/" + nodeID) + if err != nil { + return fmt.Errorf("failed to get node %s from etcd: %w", nodeID, err) + } + if resp == nil || len(resp.Kvs) == 0 { + return fmt.Errorf("node %s not found", nodeID) + } + + var node models.Node + if err := json.Unmarshal(resp.Kvs[0].Value, &node); err != nil { + return fmt.Errorf("failed to unmarshal node %s: %w", nodeID, err) + } + + want := canonicalWorkloadType(workloadType) + if want == "" { + return nil + } + + if len(node.SupportedWorkloadTypes) == 0 { + // Capability list absent on legacy node records. Only apply strict downgrade for VM. + if want == "vm" { + node.SupportedWorkloadTypes = []string{"container", "compose"} + } else { + return nil + } + } else { + filtered := make([]string, 0, len(node.SupportedWorkloadTypes)) + changed := false + for _, t := range node.SupportedWorkloadTypes { + if canonicalWorkloadType(t) == want { + changed = true + continue + } + filtered = append(filtered, canonicalWorkloadType(t)) + } + if !changed { + return nil + } + node.SupportedWorkloadTypes = filtered + } + + node.StatusReason = fmt.Sprintf("runtime for %s unavailable on agent: %s", want, strings.TrimSpace(reason)) + node.StatusUpdatedBy = "reconciler" + node.StatusUpdatedAt = time.Now().UTC() + + payload, err := json.Marshal(node) + if err != nil { + return fmt.Errorf("failed to marshal node %s: %w", nodeID, err) + } + if err := s.RetryableEtcdPut("/nodes/"+nodeID, string(payload)); err != nil { + return fmt.Errorf("failed to persist workload type capability update for node %s: %w", nodeID, err) + } + nodeLogger.WithFields(logrus.Fields{ + "node_id": nodeID, + "capability": node.SupportedWorkloadTypes, + }).Info("updated node capabilities after runtime rejection") + return nil +} + +func (s *Scheduler) markNodeNotReady(nodeID, reason, source string) error { + resp, err := s.RetryableEtcdGet("/nodes/" + nodeID) + if err != nil { + return fmt.Errorf("failed to get node %s from etcd: %w", nodeID, err) + } + if resp == nil || len(resp.Kvs) == 0 { + return fmt.Errorf("node %s not found", nodeID) + } + + var node models.Node + if err := json.Unmarshal(resp.Kvs[0].Value, &node); err != nil { + return fmt.Errorf("failed to unmarshal node %s: %w", nodeID, err) + } + + if strings.EqualFold(node.Status, "NotReady") { + incomingReason := strings.TrimSpace(reason) + // Keep current record if metadata is complete and reason/source did not change. + if strings.TrimSpace(node.StatusReason) != "" && + strings.TrimSpace(node.StatusUpdatedBy) != "" && + !node.StatusUpdatedAt.IsZero() && + (incomingReason == "" || strings.EqualFold(strings.TrimSpace(node.StatusReason), incomingReason)) && + strings.EqualFold(strings.TrimSpace(node.StatusUpdatedBy), source) { + return nil + } + node.StatusReason = incomingReason + if node.StatusReason == "" { + node.StatusReason = "node marked NotReady" + } + node.StatusUpdatedBy = source + node.StatusUpdatedAt = time.Now().UTC() + payload, err := json.Marshal(node) + if err != nil { + return fmt.Errorf("failed to marshal node %s: %w", nodeID, err) + } + if err := s.RetryableEtcdPut("/nodes/"+nodeID, string(payload)); err != nil { + return fmt.Errorf("failed to persist NotReady metadata for node %s: %w", nodeID, err) + } + _ = s.RetryableEtcdPut("/nodes/"+nodeID+"/status", node.Status) + return nil + } + node.Status = "NotReady" + node.StatusReason = strings.TrimSpace(reason) + if node.StatusReason == "" { + node.StatusReason = "node marked NotReady" + } + node.StatusUpdatedBy = source + node.StatusUpdatedAt = time.Now().UTC() + payload, err := json.Marshal(node) + if err != nil { + return fmt.Errorf("failed to marshal node %s: %w", nodeID, err) + } + if err := s.RetryableEtcdPut("/nodes/"+nodeID, string(payload)); err != nil { + return fmt.Errorf("failed to persist NotReady for node %s: %w", nodeID, err) + } + _ = s.RetryableEtcdPut("/nodes/"+nodeID+"/status", node.Status) + s.emitEvent("NodeLost", "", nodeID, reason, nil) + nodeLogger.WithFields(logrus.Fields{ + "node_id": nodeID, + "source": source, + "reason": reason, + }).Warn("marked node NotReady") + return nil +} diff --git a/persys-scheduler/internal/scheduler/reconcile.txt b/persys-scheduler/internal/scheduler/reconcile.txt new file mode 100644 index 0000000..d165c2c --- /dev/null +++ b/persys-scheduler/internal/scheduler/reconcile.txt @@ -0,0 +1,11 @@ +1. Monitor detects state change + ↓ +2. Reconciler compares desired vs actual state + ↓ +3. If drift detected, determine action needed + ↓ +4. Execute reconciliation action (recreate/restart/stop) + ↓ +5. Update workload status and metadata + ↓ +6. Log results and statistics \ No newline at end of file diff --git a/persys-scheduler/internal/scheduler/reconciler.go b/persys-scheduler/internal/scheduler/reconciler.go new file mode 100644 index 0000000..609f40d --- /dev/null +++ b/persys-scheduler/internal/scheduler/reconciler.go @@ -0,0 +1,572 @@ +package scheduler + +import ( + "context" + "encoding/json" + "fmt" + "strings" + "time" + + agentpb "github.com/persys-dev/persys-cloud/persys-scheduler/internal/agentpb" + "github.com/persys-dev/persys-cloud/persys-scheduler/internal/logging" + metricspkg "github.com/persys-dev/persys-cloud/persys-scheduler/internal/metrics" + "github.com/persys-dev/persys-cloud/persys-scheduler/internal/models" + "github.com/sirupsen/logrus" +) + +const defaultMissingGracePeriod = 15 * time.Second +const defaultNodeUnavailableGrace = 3 * time.Minute + +var reconcilerLogger = logging.C("scheduler.reconciler") + +// Reconciler handles reconciliation between desired and actual state. +type Reconciler struct { + scheduler *Scheduler + monitor *Monitor +} + +// ReconciliationResult represents the result of a reconciliation operation. +type ReconciliationResult struct { + WorkloadID string + DesiredState string + ActualState string + Action string + Success bool + ErrorMessage string + RetryCount int + LastAttempt time.Time +} + +// NewReconciler creates a new Reconciler instance. +func NewReconciler(scheduler *Scheduler, monitor *Monitor) *Reconciler { + return &Reconciler{scheduler: scheduler, monitor: monitor} +} + +// ReconcileWorkload reconciles a single workload to its desired state. +func (r *Reconciler) ReconcileWorkload(ctx context.Context, workload models.Workload) (*ReconciliationResult, error) { + if ctx == nil { + ctx = context.Background() + } + result := &ReconciliationResult{ + WorkloadID: workload.ID, + DesiredState: workload.DesiredState, + LastAttempt: time.Now(), + } + defer func() { + action := strings.TrimSpace(result.Action) + if action == "" { + action = "Unknown" + } + metricspkg.ObserveReconciliationResult(action, result.Success) + }() + + // Respect retry backoff windows to avoid hammering unavailable nodes. + if !workload.Retry.NextRetryAt.IsZero() && time.Now().UTC().Before(workload.Retry.NextRetryAt) { + result.Action = "BackoffWait" + result.Success = true + return result, nil + } + + if strings.EqualFold(workload.DesiredState, "Deleted") && workload.NodeID == "" { + if err := r.scheduler.DeleteWorkload(workload.ID); err != nil { + result.Action = "FinalizeDelete" + result.Success = false + result.ErrorMessage = err.Error() + r.scheduler.writeReconciliationRecord(workload.ID, result.Action, false, result.ErrorMessage) + r.updateWorkloadReconciliationStatus(workload.ID, result) + return result, err + } + result.Action = "FinalizeDelete" + result.Success = true + r.scheduler.writeReconciliationRecord(workload.ID, result.Action, true, "Deleted from state store") + return result, nil + } + + if err := r.scheduler.EnsureWorkloadAssigned(&workload); err != nil && !strings.EqualFold(workload.DesiredState, "Deleted") { + result.ActualState = "Unknown" + result.Action = "Assign" + result.Success = false + result.ErrorMessage = fmt.Sprintf("failed to assign node: %v", err) + r.scheduler.writeReconciliationRecord(workload.ID, result.Action, false, result.ErrorMessage) + r.updateWorkloadReconciliationStatus(workload.ID, result) + return result, err + } + + if r.scheduler.dueForRetry(workload) { + workload.Retry.NextRetryAt = time.Time{} + workload.Status = "Pending" + if workload.Metadata == nil { + workload.Metadata = map[string]interface{}{} + } + workload.Metadata["last_action"] = "RetryDue" + _ = r.scheduler.saveWorkload(workload) + } + + if strings.TrimSpace(workload.NodeID) != "" && !strings.EqualFold(workload.DesiredState, "Deleted") { + waiting, failoverErr := r.handleUnavailableAssignedNode(&workload) + if failoverErr != nil { + result.ActualState = "Unknown" + result.Action = "Reschedule" + result.Success = false + result.ErrorMessage = failoverErr.Error() + r.scheduler.writeReconciliationRecord(workload.ID, result.Action, false, result.ErrorMessage) + _ = r.scheduler.UpdateWorkloadRetryOnFailure(workload.ID, result.ErrorMessage) + r.updateWorkloadReconciliationStatus(workload.ID, result) + return result, failoverErr + } + if waiting { + result.ActualState = "Unknown" + result.Action = "AwaitFailover" + result.Success = true + r.scheduler.writeReconciliationRecord(workload.ID, result.Action, true, "node unavailable; waiting for retry window") + r.updateWorkloadReconciliationStatus(workload.ID, result) + return result, nil + } + } + + actualState, err := r.getActualWorkloadState(ctx, workload) + if err != nil { + if strings.TrimSpace(workload.NodeID) != "" && isNodeUnreachableError(err) && !strings.EqualFold(workload.DesiredState, "Deleted") { + _ = r.scheduler.MarkNodeNotReady(workload.NodeID, err.Error()) + waiting, failoverErr := r.handleUnavailableAssignedNode(&workload) + if failoverErr != nil { + result.ActualState = "Unknown" + result.Action = "Reschedule" + result.Success = false + result.ErrorMessage = failoverErr.Error() + r.scheduler.writeReconciliationRecord(workload.ID, result.Action, false, result.ErrorMessage) + _ = r.scheduler.UpdateWorkloadRetryOnFailure(workload.ID, result.ErrorMessage) + r.updateWorkloadReconciliationStatus(workload.ID, result) + return result, failoverErr + } + if waiting { + result.ActualState = "Unknown" + result.Action = "AwaitFailover" + result.Success = true + r.scheduler.writeReconciliationRecord(workload.ID, result.Action, true, "node unreachable; waiting for retry window") + r.updateWorkloadReconciliationStatus(workload.ID, result) + return result, nil + } + // Node changed; re-read actual state on newly assigned node. + actualState, err = r.getActualWorkloadState(ctx, workload) + if err == nil { + result.ActualState = actualState + } + } + } + if err != nil { + result.ActualState = "Unknown" + result.Action = "Investigate" + result.Success = false + result.ErrorMessage = fmt.Sprintf("failed to get actual state: %v", err) + r.scheduler.writeReconciliationRecord(workload.ID, result.Action, false, result.ErrorMessage) + r.updateWorkloadReconciliationStatus(workload.ID, result) + return result, err + } + result.ActualState = actualState + + if r.needsReconciliation(workload, actualState) { + action, err := r.performReconciliation(ctx, workload, actualState) + result.Action = action + result.Success = err == nil + if err != nil { + if handled, hErr := r.handleRuntimeUnavailable(&workload, err); handled { + if hErr != nil { + result.Success = false + result.ErrorMessage = hErr.Error() + _ = r.scheduler.UpdateWorkloadRetryOnFailure(workload.ID, result.ErrorMessage) + } else { + result.Action = "ReschedulePending" + result.Success = true + result.ErrorMessage = "" + } + } else { + result.ErrorMessage = err.Error() + _ = r.scheduler.UpdateWorkloadRetryOnFailure(workload.ID, result.ErrorMessage) + } + } + } else { + result.Action = "NoAction" + result.Success = true + } + + r.scheduler.writeReconciliationRecord(workload.ID, result.Action, result.Success, result.ErrorMessage) + r.updateWorkloadReconciliationStatus(workload.ID, result) + return result, nil +} + +func (r *Reconciler) handleRuntimeUnavailable(workload *models.Workload, applyErr error) (bool, error) { + if workload == nil || strings.TrimSpace(workload.NodeID) == "" || applyErr == nil { + return false, nil + } + msg := strings.ToLower(applyErr.Error()) + if !strings.Contains(msg, "runtime not available") { + return false, nil + } + + oldNode := workload.NodeID + if err := r.scheduler.MarkNodeWorkloadTypeUnsupported(oldNode, workload.Type, applyErr.Error()); err != nil { + return true, err + } + + // Force reassignment on next reconciliation cycle. + workload.NodeID = "" + workload.AssignedNode = "" + workload.Status = "Pending" + workload.StatusInfo.LastUpdated = time.Now().UTC() + if workload.Metadata == nil { + workload.Metadata = map[string]interface{}{} + } + workload.Metadata["last_action"] = "RuntimeUnsupportedReschedulePending" + workload.Metadata["last_error"] = applyErr.Error() + + if err := r.scheduler.saveWorkload(*workload); err != nil { + return true, err + } + ctx, cancel := context.WithTimeout(context.Background(), etcdTimeout) + defer cancel() + _, _ = r.scheduler.etcdClient.Delete(ctx, assignmentKey(workload.ID)) + _ = r.scheduler.UpdateWorkloadLogs(workload.ID, fmt.Sprintf("Runtime unavailable on node %s; scheduling on another node", oldNode)) + r.scheduler.emitEvent("Rescheduled", workload.ID, oldNode, "runtime unsupported on assigned node", map[string]interface{}{"workload_type": workload.Type}) + reconcilerLogger.WithFields(logrus.Fields{ + "workload_id": workload.ID, + "old_node_id": oldNode, + }).Warn("workload marked for reassignment after runtime unavailable") + return true, nil +} + +func (r *Reconciler) handleUnavailableAssignedNode(workload *models.Workload) (bool, error) { + node, err := r.scheduler.GetNodeByID(workload.NodeID) + if err != nil { + return false, nil + } + + unavailableGrace := schedulerDurationEnv("SCHEDULER_NODE_UNAVAILABLE_GRACE", defaultNodeUnavailableGrace) + nodeUnavailable := (!strings.EqualFold(node.Status, "Ready") && !strings.EqualFold(node.Status, "Active")) || + time.Since(node.LastHeartbeat) > unavailableGrace + if !nodeUnavailable { + return false, nil + } + + oldNode := workload.NodeID + nextNode, reason, selErr := r.scheduler.selectNodeForWorkload(*workload) + if selErr != nil { + reconcilerLogger.WithError(selErr).WithFields(logrus.Fields{ + "workload_id": workload.ID, + "old_node_id": oldNode, + }).Warn("failover pending: no suitable node available") + if err := r.scheduler.UpdateWorkloadRetryOnFailure(workload.ID, fmt.Sprintf("node %s unavailable; no failover target: %v", oldNode, selErr)); err != nil { + return false, err + } + return true, nil + } + + if strings.EqualFold(nextNode.NodeID, oldNode) { + reconcilerLogger.WithFields(logrus.Fields{ + "workload_id": workload.ID, + "node_id": oldNode, + }).Warn("failover deferred: selected same node") + if err := r.scheduler.UpdateWorkloadRetryOnFailure(workload.ID, fmt.Sprintf("node %s unavailable; selected same node", oldNode)); err != nil { + return false, err + } + return true, nil + } + + if workload.Metadata == nil { + workload.Metadata = map[string]interface{}{} + } + workload.Metadata["previous_node"] = oldNode + workload.Metadata["last_action"] = "Rescheduled" + workload.Retry.Attempts = 0 + workload.Retry.NextRetryAt = time.Time{} + + failoverReason := fmt.Sprintf("node %s unavailable; %s", oldNode, reason) + if err := r.scheduler.assignWorkload(workload, nextNode, failoverReason); err != nil { + return false, err + } + reconcilerLogger.WithFields(logrus.Fields{ + "workload_id": workload.ID, + "from_node": oldNode, + "to_node": nextNode.NodeID, + }).Info("rescheduled workload") + _ = r.scheduler.UpdateWorkloadLogs(workload.ID, fmt.Sprintf("Rescheduled from node %s to %s", oldNode, nextNode.NodeID)) + r.scheduler.emitEvent("Rescheduled", workload.ID, nextNode.NodeID, failoverReason, map[string]interface{}{"previous_node": oldNode}) + return false, nil +} + +// getActualWorkloadState queries the agent to get the actual state of a workload. +func (r *Reconciler) getActualWorkloadState(ctx context.Context, workload models.Workload) (string, error) { + node, err := r.scheduler.GetNodeByID(workload.NodeID) + if err != nil { + return "", fmt.Errorf("failed to get node %s: %v", workload.NodeID, err) + } + + statusResp, err := r.scheduler.getWorkloadStatusFromNode(ctx, node, workload.ID) + if err != nil { + if isWorkloadStatusNotFound(err) { + return "Missing", nil + } + return "", fmt.Errorf("failed to get workload status: %v", err) + } + if statusResp == nil { + return "Unknown", nil + } + return mapActualStateToSchedulerStatus(statusResp.GetActualState()), nil +} + +// needsReconciliation determines if a workload needs reconciliation. +func (r *Reconciler) needsReconciliation(workload models.Workload, actualState string) bool { + desiredState := strings.TrimSpace(workload.DesiredState) + if desiredState == "" { + desiredState = "Running" + } + + if actualState == "Missing" || actualState == "Pending" { + if workload.Metadata != nil { + if lastLaunchRaw, ok := workload.Metadata["lastLaunchTime"]; ok { + grace := schedulerDurationEnv("SCHEDULER_MISSING_GRACE_PERIOD", defaultMissingGracePeriod) + switch v := lastLaunchRaw.(type) { + case string: + if t, err := time.Parse(time.RFC3339, v); err == nil && time.Since(t) < grace { + return false + } + case time.Time: + if time.Since(v) < grace { + return false + } + } + } + } + } + + if strings.EqualFold(actualState, desiredState) { + return false + } + if strings.EqualFold(desiredState, "Stopped") && strings.EqualFold(actualState, "Stopped") { + return false + } + return true +} + +// performReconciliation performs the actual reconciliation action. +func (r *Reconciler) performReconciliation(ctx context.Context, workload models.Workload, actualState string) (string, error) { + desiredState := strings.TrimSpace(workload.DesiredState) + if desiredState == "" { + desiredState = "Running" + } + + reconcilerLogger.WithFields(logrus.Fields{ + "workload_id": workload.ID, + "actual_state": actualState, + "desired_state": desiredState, + }).Debug("reconciling workload") + + switch { + case strings.EqualFold(desiredState, "Deleted"): + return r.deleteDesiredWorkload(ctx, workload) + case strings.EqualFold(desiredState, "Running") && (strings.EqualFold(actualState, "Missing") || strings.EqualFold(actualState, "Stopped") || strings.EqualFold(actualState, "Failed") || strings.EqualFold(actualState, "Unknown")): + return r.applyDesiredState(ctx, workload, agentpb.DesiredState_DESIRED_STATE_RUNNING, "ReapplyRunning") + case strings.EqualFold(desiredState, "Stopped") && (strings.EqualFold(actualState, "Running") || strings.EqualFold(actualState, "Pending") || strings.EqualFold(actualState, "Unknown")): + return r.applyDesiredState(ctx, workload, agentpb.DesiredState_DESIRED_STATE_STOPPED, "ApplyStopped") + default: + return "NoAction", nil + } +} + +func (r *Reconciler) applyDesiredState(ctx context.Context, workload models.Workload, desired agentpb.DesiredState, action string) (string, error) { + node, err := r.scheduler.GetNodeByID(workload.NodeID) + if err != nil { + return action, fmt.Errorf("failed to get node: %v", err) + } + + if desired == agentpb.DesiredState_DESIRED_STATE_STOPPED { + workload.DesiredState = "Stopped" + } else { + workload.DesiredState = "Running" + } + + resp, err := r.scheduler.applyWorkloadOnNode(ctx, node, workload) + if err != nil { + return action, fmt.Errorf("failed to apply desired state: %v", err) + } + if resp == nil { + return action, fmt.Errorf("apply response was nil") + } + if !resp.GetApplied() && !resp.GetSkipped() { + return action, fmt.Errorf("agent rejected apply: %s", strings.TrimSpace(resp.GetMessage())) + } + + if resp.Status != nil { + _ = r.scheduler.UpdateWorkloadStatus(workload.ID, mapActualStateToSchedulerStatus(resp.Status.ActualState)) + } + _ = r.scheduler.UpdateWorkloadLogs(workload.ID, fmt.Sprintf("Reconciler action %s at %s", action, time.Now().Format(time.RFC3339))) + r.scheduler.emitEvent("WorkloadScheduled", workload.ID, workload.NodeID, action, nil) + + if workload.Metadata == nil { + workload.Metadata = make(map[string]interface{}) + } + workload.Metadata["lastLaunchTime"] = time.Now().Format(time.RFC3339) + + workloadJSON, marshalErr := json.Marshal(workload) + if marshalErr == nil { + _ = r.scheduler.RetryableEtcdPut("/workloads/"+workload.ID, string(workloadJSON)) + } + + return action, nil +} + +func (r *Reconciler) deleteDesiredWorkload(ctx context.Context, workload models.Workload) (string, error) { + if workload.NodeID != "" { + node, err := r.scheduler.GetNodeByID(workload.NodeID) + if err == nil { + if _, err := r.scheduler.deleteWorkloadFromNode(ctx, node, workload.ID); err != nil && !isWorkloadStatusNotFound(err) { + return "Delete", fmt.Errorf("failed deleting workload on node: %v", err) + } + } + } + if err := r.scheduler.DeleteWorkloadWithContext(ctx, workload.ID); err != nil { + return "Delete", err + } + r.scheduler.emitEvent("Rescheduled", workload.ID, workload.NodeID, "Deleted workload", nil) + return "Delete", nil +} + +// updateWorkloadReconciliationStatus updates the workload with reconciliation metadata. +func (r *Reconciler) updateWorkloadReconciliationStatus(workloadID string, result *ReconciliationResult) { + workload, err := r.scheduler.GetWorkloadByID(workloadID) + if err != nil { + reconcilerLogger.WithError(err).WithField("workload_id", workloadID).Warn("failed to fetch workload for reconciliation status update") + return + } + + if workload.Metadata == nil { + workload.Metadata = make(map[string]interface{}) + } + workload.Metadata["lastReconciliation"] = result.LastAttempt.Format(time.RFC3339) + workload.Metadata["lastReconciliationAction"] = result.Action + workload.Metadata["lastReconciliationSuccess"] = result.Success + workload.Metadata["reconciliationRetryCount"] = result.RetryCount + + workloadJSON, err := json.Marshal(workload) + if err != nil { + reconcilerLogger.WithError(err).WithField("workload_id", workloadID).Warn("failed to marshal workload during reconciliation status update") + return + } + if err := r.scheduler.RetryableEtcdPut("/workloads/"+workloadID, string(workloadJSON)); err != nil { + reconcilerLogger.WithError(err).WithField("workload_id", workloadID).Warn("failed to persist reconciliation status") + } +} + +// ReconcileAllWorkloads reconciles all workloads in the system. +func (r *Reconciler) ReconcileAllWorkloads(ctx context.Context) ([]*ReconciliationResult, error) { + if ctx == nil { + ctx = context.Background() + } + workloads, err := r.scheduler.GetWorkloads() + if err != nil { + return nil, fmt.Errorf("failed to get workloads: %v", err) + } + + var results []*ReconciliationResult + for _, workload := range workloads { + if workload.Status == "Completed" || workload.Status == "Deleted" { + continue + } + result, err := r.ReconcileWorkload(ctx, workload) + if err != nil { + reconcilerLogger.WithError(err).WithField("workload_id", workload.ID).Warn("failed to reconcile workload") + continue + } + results = append(results, result) + } + return results, nil +} + +// StartReconciliationLoop starts the continuous reconciliation loop. +func (r *Reconciler) StartReconciliationLoop(ctx context.Context, interval time.Duration) { + ticker := time.NewTicker(interval) + defer ticker.Stop() + + reconcilerLogger.WithField("interval", interval.String()).Info("starting reconciliation loop") + for { + select { + case <-ctx.Done(): + reconcilerLogger.Info("stopping reconciliation loop") + return + case <-ticker.C: + cycleStart := time.Now() + results, err := r.ReconcileAllWorkloads(ctx) + metricspkg.ObserveReconciliationCycle(time.Since(cycleStart), err) + if err != nil { + reconcilerLogger.WithError(err).Error("reconciliation cycle failed") + continue + } + successCount := 0 + actionCount := 0 + failureCount := 0 + for _, result := range results { + if result.Success { + successCount++ + } else { + failureCount++ + } + if result.Action != "NoAction" { + actionCount++ + } + } + if actionCount > 0 || failureCount > 0 { + reconcilerLogger.WithFields(logrus.Fields{ + "workloads_processed": len(results), + "actions": actionCount, + "successful": successCount, + "failed": failureCount, + }).Info("reconciliation cycle summary") + } + if err := r.scheduler.RefreshStateMetrics(); err != nil { + reconcilerLogger.WithError(err).Warn("failed to refresh scheduler state metrics") + } + } + } +} + +// GetReconciliationStats returns statistics about reconciliation performance. +func (r *Reconciler) GetReconciliationStats() (map[string]interface{}, error) { + workloads, err := r.scheduler.GetWorkloads() + if err != nil { + return nil, err + } + + stats := map[string]interface{}{ + "totalWorkloads": len(workloads), + "needingReconciliation": 0, + "reconciliationErrors": 0, + "lastReconciliation": "", + } + + latest := time.Time{} + for _, workload := range workloads { + if workload.Metadata != nil { + if lastRecon, ok := workload.Metadata["lastReconciliation"]; ok { + if s, ok := lastRecon.(string); ok { + if t, err := time.Parse(time.RFC3339, s); err == nil && t.After(latest) { + latest = t + } + } + } + if success, ok := workload.Metadata["lastReconciliationSuccess"]; ok { + if s, ok := success.(bool); ok && !s { + stats["reconciliationErrors"] = stats["reconciliationErrors"].(int) + 1 + } + } + } + actualState, err := r.getActualWorkloadState(context.Background(), workload) + if err == nil && r.needsReconciliation(workload, actualState) { + stats["needingReconciliation"] = stats["needingReconciliation"].(int) + 1 + } + } + if !latest.IsZero() { + stats["lastReconciliation"] = latest.Format(time.RFC3339) + } + + return stats, nil +} diff --git a/persys-scheduler/internal/scheduler/scheduler.go b/persys-scheduler/internal/scheduler/scheduler.go new file mode 100644 index 0000000..72607c2 --- /dev/null +++ b/persys-scheduler/internal/scheduler/scheduler.go @@ -0,0 +1,695 @@ +package scheduler + +import ( + "context" + "encoding/json" + "fmt" + "os" + "sort" + "strings" + "sync" + "time" + + "github.com/google/uuid" + "github.com/persys-dev/persys-cloud/persys-scheduler/internal/logging" + "github.com/persys-dev/persys-cloud/persys-scheduler/internal/models" + "github.com/sirupsen/logrus" + clientv3 "go.etcd.io/etcd/client/v3" +) + +// Constants +const ( + etcdTimeout = 5 * time.Second + maxRetries = 5 + retryWaitTime = 2 * time.Second +) + +var schedulerLogger = logging.C("scheduler.core") + +// Scheduler holds the state and configuration for the cluster scheduler. +type Scheduler struct { + etcdClient *clientv3.Client + domain string + monitor *Monitor + reconciler *Reconciler + bgWG sync.WaitGroup +} + +// NewScheduler initializes the scheduler with an etcd client and configuration. +func NewScheduler() (*Scheduler, error) { + // Get etcd endpoints from environment variable, with fallback + etcdEndpoints := os.Getenv("ETCD_ENDPOINTS") + if etcdEndpoints == "" { + etcdEndpoints = "localhost:2379" + } + + // Get domain from environment variable, with fallback + domain := os.Getenv("DOMAIN") + if domain == "" { + domain = "persys.local" + } + + cli, err := clientv3.New(clientv3.Config{ + Endpoints: strings.Split(etcdEndpoints, ","), + DialTimeout: etcdTimeout, + }) + if err != nil { + return nil, fmt.Errorf("failed to create etcd client: %v", err) + } + + // Verify etcd connection + ctx, cancel := context.WithTimeout(context.Background(), etcdTimeout) + defer cancel() + if _, err := cli.Get(ctx, "/health"); err != nil { + cli.Close() + return nil, fmt.Errorf("failed to connect to etcd: %v", err) + } + + scheduler := &Scheduler{etcdClient: cli, domain: domain} + + // Initialize monitor and reconciler + scheduler.monitor = NewMonitor(scheduler) + scheduler.reconciler = NewReconciler(scheduler, scheduler.monitor) + + return scheduler, nil +} + +// Close shuts down the scheduler gracefully. +func (s *Scheduler) Close() error { + if s.etcdClient != nil { + return s.etcdClient.Close() + } + return nil +} + +func (s *Scheduler) RegisterNode(node models.Node) error { + if node.NodeID == "" || node.IPAddress == "" || node.AgentPort == 0 { + return fmt.Errorf("nodeID, IPAddress, and AgentPort are required") + } + if node.TotalCPU <= 0 || node.TotalMemory <= 0 { + return fmt.Errorf("totalCPU and totalMemory must be positive") + } + + node.LastHeartbeat = time.Now() + node.DomainName = node.NodeID + "." + s.domain + if node.Status == "" { + node.Status = "Ready" + } + node.StatusReason = "registered" + node.StatusUpdatedBy = "register" + node.StatusUpdatedAt = time.Now().UTC() + if node.AvailableCPU == 0 { + node.AvailableCPU = node.TotalCPU + } + if node.AvailableMemory == 0 { + node.AvailableMemory = node.TotalMemory + } + if node.Labels == nil { + node.Labels = make(map[string]string) + } + + schedulerLogger.WithFields(logrus.Fields{ + "node_id": node.NodeID, + "endpoint": node.AgentEndpoint, + "status": node.Status, + "supported_types": node.SupportedWorkloadTypes, + "total_cpu": node.TotalCPU, + "total_memory_mb": node.TotalMemory, + "available_cpu": node.AvailableCPU, + "available_memory": node.AvailableMemory, + }).Info("registering node") + + nodeJSON, err := json.Marshal(node) + if err != nil { + return fmt.Errorf("failed to marshal node: %v", err) + } + + if err := s.RetryableEtcdPut("/nodes/"+node.NodeID, string(nodeJSON)); err != nil { + return fmt.Errorf("failed to register node %s: %v", node.NodeID, err) + } + _ = s.RetryableEtcdPut("/nodes/"+node.NodeID+"/status", node.Status) + + if err := s.UpdateCoreDNS(node); err != nil { + schedulerLogger.WithError(err).WithField("node_id", node.NodeID).Warn("failed to update CoreDNS for node") + } + + schedulerLogger.WithField("node_id", node.NodeID).Info("registered node") + + return nil +} + +// matchesLabels checks if workload labels are a subset of node labels +func matchesLabels(workloadLabels, nodeLabels map[string]string) bool { + if len(workloadLabels) == 0 { + return true // No labels to match, accept node + } + for k, v := range workloadLabels { + if nodeVal, ok := nodeLabels[k]; !ok || nodeVal != v { + return false + } + } + return true +} + +func nodeUtilizationScore(node models.Node) float64 { + cpuTotal := node.TotalCPU + memTotal := float64(node.TotalMemory) + if cpuTotal <= 0 || memTotal <= 0 { + return 1e9 + } + cpuUsed := cpuTotal - node.AvailableCPU + memUsed := memTotal - float64(node.AvailableMemory) + cpuRatio := cpuUsed / cpuTotal + memRatio := memUsed / memTotal + return (cpuRatio + memRatio) / 2.0 +} + +func canonicalWorkloadType(t string) string { + switch strings.ToLower(strings.TrimSpace(t)) { + case "docker-container", "container": + return "container" + case "docker-compose", "compose": + return "compose" + case "vm": + return "vm" + default: + return strings.ToLower(strings.TrimSpace(t)) + } +} + +func nodeSupportsWorkloadType(node models.Node, workloadType string) bool { + want := canonicalWorkloadType(workloadType) + if want == "" { + return true + } + + if len(node.SupportedWorkloadTypes) > 0 { + for _, t := range node.SupportedWorkloadTypes { + if canonicalWorkloadType(t) == want { + return true + } + } + return false + } + + // Backward compatibility for nodes registered before capability wiring. + // VM scheduling is strict: require explicit hypervisor signal when capability list is absent. + if want == "vm" { + return strings.TrimSpace(node.Hypervisor.Type) != "" || + strings.EqualFold(node.Hypervisor.Status, "active") || + strings.EqualFold(node.Hypervisor.Status, "ready") + } + return true +} + +func isNodeStatusSubKey(key string) bool { + return strings.HasSuffix(key, "/status") +} + +func (s *Scheduler) selectNodeForWorkload(workload models.Workload) (models.Node, string, error) { + resp, err := s.RetryableEtcdGet(nodesPrefix, clientv3.WithPrefix()) + if err != nil { + return models.Node{}, "", fmt.Errorf("failed to get nodes for scheduling: %v", err) + } + if len(resp.Kvs) == 0 { + return models.Node{}, "", fmt.Errorf("no nodes available") + } + + candidates := make([]models.Node, 0) + rejections := make([]string, 0) + for _, kv := range resp.Kvs { + if isNodeStatusSubKey(string(kv.Key)) { + continue + } + var node models.Node + if err := json.Unmarshal(kv.Value, &node); err != nil { + schedulerLogger.WithError(err).WithField("key", string(kv.Key)).Warn("failed to unmarshal node data") + continue + } + if !strings.EqualFold(node.Status, "active") && !strings.EqualFold(node.Status, "ready") { + reason := strings.TrimSpace(node.StatusReason) + if reason == "" { + reason = "unspecified" + } + rejections = append(rejections, fmt.Sprintf("%s: status=%s reason=%q by=%s at=%s", node.NodeID, node.Status, reason, node.StatusUpdatedBy, node.StatusUpdatedAt.UTC().Format(time.RFC3339))) + continue + } + if time.Since(node.LastHeartbeat) > 10*time.Minute { + rejections = append(rejections, fmt.Sprintf("%s: heartbeat_stale last=%s", node.NodeID, node.LastHeartbeat.UTC().Format(time.RFC3339))) + continue + } + if !matchesLabels(workload.Labels, node.Labels) { + rejections = append(rejections, fmt.Sprintf("%s: label_mismatch", node.NodeID)) + continue + } + if !nodeSupportsWorkloadType(node, workload.Type) { + rejections = append(rejections, fmt.Sprintf("%s: workload_type_unsupported need=%s supports=%v", node.NodeID, canonicalWorkloadType(workload.Type), node.SupportedWorkloadTypes)) + continue + } + if workload.Resources.CPUUsage > 0 && node.AvailableCPU < workload.Resources.CPUUsage { + rejections = append(rejections, fmt.Sprintf("%s: cpu_insufficient need=%.3f have=%.3f", node.NodeID, workload.Resources.CPUUsage, node.AvailableCPU)) + continue + } + if workload.Resources.MemoryUsage > 0 && float64(node.AvailableMemory) < workload.Resources.MemoryUsage { + rejections = append(rejections, fmt.Sprintf("%s: memory_insufficient need=%.0fMB have=%dMB", node.NodeID, workload.Resources.MemoryUsage, node.AvailableMemory)) + continue + } + if workload.Resources.DiskUsage >= 0 { + availableDisk := int(node.TotalMemory - node.AvailableMemory) // placeholder until explicit disk tracking exists on node model + _ = availableDisk + } + candidates = append(candidates, node) + } + if len(candidates) == 0 { + if len(rejections) > 0 { + return models.Node{}, "", fmt.Errorf("no suitable node available (%s)", strings.Join(rejections, "; ")) + } + return models.Node{}, "", fmt.Errorf("no suitable node available") + } + + sort.Slice(candidates, func(i, j int) bool { + return nodeUtilizationScore(candidates[i]) < nodeUtilizationScore(candidates[j]) + }) + + reason := fmt.Sprintf("selected by lowest utilization score %.4f", nodeUtilizationScore(candidates[0])) + return candidates[0], reason, nil +} + +func (s *Scheduler) assignWorkload(workload *models.Workload, node models.Node, reason string) error { + if workload.Metadata == nil { + workload.Metadata = map[string]interface{}{} + } + workload.NodeID = node.NodeID + workload.AssignedNode = node.NodeID + workload.Status = "Scheduled" + workload.StatusInfo.ActualState = "Pending" + workload.StatusInfo.LastUpdated = time.Now().UTC() + workload.Metadata["last_action"] = "Assigned" + workload.Metadata["assignment_reason"] = reason + + if err := s.saveWorkload(*workload); err != nil { + return err + } + if err := s.writeAssignment(workload.ID, node.NodeID, reason); err != nil { + return err + } + s.emitEvent("WorkloadScheduled", workload.ID, node.NodeID, reason, nil) + return nil +} + +// ScheduleWorkload assigns a workload to a suitable node and sends a command via the agent API. +func (s *Scheduler) ScheduleWorkload(workload models.Workload) (string, error) { + if workload.ID == "" { + workload.ID = uuid.New().String() + schedulerLogger.WithField("workload_id", workload.ID).Debug("generated workload ID") + } + s.ensureWorkloadRevision(&workload) + s.initializeWorkloadDefaults(&workload) + + selectedNode, reason, err := s.selectNodeForWorkload(workload) + if err != nil { + s.emitEvent("WorkloadFailed", workload.ID, "", err.Error(), nil) + return "", err + } + + if err := s.assignWorkload(&workload, selectedNode, reason); err != nil { + return "", fmt.Errorf("failed assigning workload %s: %w", workload.ID, err) + } + + applyResp, err := s.applyWorkloadOnNode(context.Background(), selectedNode, workload) + if err != nil { + _ = s.UpdateWorkloadStatus(workload.ID, "Failed") + _ = s.UpdateWorkloadLogs(workload.ID, fmt.Sprintf("ApplyWorkload failed: %v", err)) + s.emitEvent("WorkloadFailed", workload.ID, selectedNode.NodeID, err.Error(), nil) + return "", fmt.Errorf("apply workload on node %s: %v", selectedNode.NodeID, err) + } + if applyResp == nil || (!applyResp.GetApplied() && !applyResp.GetSkipped()) { + msg := "agent rejected apply request" + if applyResp != nil && strings.TrimSpace(applyResp.GetMessage()) != "" { + msg = applyResp.GetMessage() + } + _ = s.UpdateWorkloadStatus(workload.ID, "Failed") + _ = s.UpdateWorkloadLogs(workload.ID, fmt.Sprintf("ApplyWorkload rejected: %s", msg)) + s.emitEvent("WorkloadFailed", workload.ID, selectedNode.NodeID, msg, nil) + return "", fmt.Errorf("apply workload on node %s rejected: %s", selectedNode.NodeID, msg) + } + + if applyResp != nil && applyResp.Status != nil { + _ = s.UpdateWorkloadStatus(workload.ID, mapActualStateToSchedulerStatus(applyResp.Status.ActualState)) + } + if applyResp != nil { + _ = s.UpdateWorkloadLogs(workload.ID, fmt.Sprintf("Apply response: applied=%t skipped=%t message=%s", applyResp.Applied, applyResp.Skipped, applyResp.Message)) + } + + terminalStatus, actions, waitErr := s.waitForWorkloadTerminalStatus(context.Background(), selectedNode, workload, s.applyTimeoutFor(workload)) + if waitErr != nil { + _ = s.UpdateWorkloadStatus(workload.ID, "Unknown") + _ = s.UpdateWorkloadLogs(workload.ID, fmt.Sprintf("Timed out waiting for terminal state: %v", waitErr)) + if len(actions) > 0 { + _ = s.UpdateWorkloadLogs(workload.ID, fmt.Sprintf("Recent actions: %d action(s) captured", len(actions))) + } + return "", waitErr + } + + if terminalStatus != nil { + _ = s.UpdateWorkloadStatus(workload.ID, mapActualStateToSchedulerStatus(terminalStatus.GetActualState())) + if terminalStatus.GetMessage() != "" { + _ = s.UpdateWorkloadLogs(workload.ID, terminalStatus.GetMessage()) + } + } + s.writeReconciliationRecord(workload.ID, "Apply", true, "Converged") + + schedulerLogger.WithFields(logrus.Fields{ + "workload_id": workload.ID, + "node_id": selectedNode.NodeID, + }).Info("workload converged") + return selectedNode.NodeID, nil +} + +// GetNodes retrieves all nodes from etcd. +func (s *Scheduler) GetNodes() ([]models.Node, error) { + resp, err := s.RetryableEtcdGet("/nodes/", clientv3.WithPrefix()) + if err != nil { + return nil, fmt.Errorf("failed to get nodes: %v", err) + } + + nodes := make([]models.Node, 0) + if resp == nil { + return nodes, nil + } + + for _, kv := range resp.Kvs { + if isNodeStatusSubKey(string(kv.Key)) { + continue + } + var node models.Node + if err := json.Unmarshal(kv.Value, &node); err != nil { + schedulerLogger.WithError(err).WithField("key", string(kv.Key)).Warn("failed to unmarshal node data") + continue + } + nodes = append(nodes, node) + } + + return nodes, nil +} + +// GetNodeByID retrieves a specific node by ID. +func (s *Scheduler) GetNodeByID(nodeID string) (models.Node, error) { + resp, err := s.RetryableEtcdGet("/nodes/" + nodeID) + if err != nil { + return models.Node{}, fmt.Errorf("failed to get node %s: %v", nodeID, err) + } + + if len(resp.Kvs) == 0 { + return models.Node{}, fmt.Errorf("node %s not found", nodeID) + } + + var node models.Node + if err := json.Unmarshal(resp.Kvs[0].Value, &node); err != nil { + return models.Node{}, fmt.Errorf("failed to unmarshal node %s: %v", nodeID, err) + } + + return node, nil +} + +// DeleteNode removes a node from etcd and CoreDNS. +func (s *Scheduler) DeleteNode(nodeID string) error { + ctx, cancel := context.WithTimeout(context.Background(), etcdTimeout) + defer cancel() + _, err := s.etcdClient.Delete(ctx, "/nodes/"+nodeID) + if err != nil { + return fmt.Errorf("failed to delete node %s: %v", nodeID, err) + } + _, _ = s.etcdClient.Delete(ctx, "/nodes/"+nodeID+"/status") + + // Remove from CoreDNS + key := fmt.Sprintf("/skydns/%s/%s", reverseDomain(s.domain), nodeID) + _, err = s.etcdClient.Delete(ctx, key) + if err != nil { + schedulerLogger.WithError(err).WithField("node_id", nodeID).Warn("failed to remove CoreDNS entry") + } + + schedulerLogger.WithField("node_id", nodeID).Info("deleted node") + return nil +} + +// GetWorkloads retrieves all workloads from etcd. +func (s *Scheduler) GetWorkloads() ([]models.Workload, error) { + resp, err := s.RetryableEtcdGet("/workloads/", clientv3.WithPrefix()) + if err != nil { + return nil, fmt.Errorf("failed to get workloads: %v", err) + } + + workloads := make([]models.Workload, 0) + if resp == nil { + return workloads, nil + } + + for _, kv := range resp.Kvs { + var workload models.Workload + if err := json.Unmarshal(kv.Value, &workload); err != nil { + schedulerLogger.WithError(err).WithField("key", string(kv.Key)).Warn("failed to unmarshal workload data") + continue + } + workloads = append(workloads, workload) + } + + return workloads, nil +} + +// GetWorkloadByID retrieves a specific workload by ID. +func (s *Scheduler) GetWorkloadByID(workloadID string) (models.Workload, error) { + resp, err := s.RetryableEtcdGet("/workloads/" + workloadID) + if err != nil { + return models.Workload{}, fmt.Errorf("failed to get workload %s: %v", workloadID, err) + } + + if len(resp.Kvs) == 0 { + return models.Workload{}, fmt.Errorf("workload %s not found", workloadID) + } + + var workload models.Workload + if err := json.Unmarshal(resp.Kvs[0].Value, &workload); err != nil { + return models.Workload{}, fmt.Errorf("failed to unmarshal workload %s: %v", workloadID, err) + } + + return workload, nil +} + +// DeleteWorkload removes a workload from etcd. +func (s *Scheduler) DeleteWorkloadWithContext(ctx context.Context, workloadID string) error { + if ctx == nil { + ctx = context.Background() + } + workload, err := s.GetWorkloadByID(workloadID) + if err == nil && workload.NodeID != "" { + node, nodeErr := s.GetNodeByID(workload.NodeID) + if nodeErr == nil { + if _, delErr := s.deleteWorkloadFromNode(ctx, node, workloadID); delErr != nil { + return fmt.Errorf("delete workload on node %s: %v", node.NodeID, delErr) + } + + deadline := time.Now().Add(s.deleteTimeout()) + for { + _, stErr := s.getWorkloadStatusFromNode(ctx, node, workloadID) + if stErr != nil && isWorkloadStatusNotFound(stErr) { + break + } + if time.Now().After(deadline) { + return fmt.Errorf("timeout waiting for workload %s deletion on node %s", workloadID, node.NodeID) + } + time.Sleep(s.agentPollInterval()) + } + } + } + + ctx, cancel := context.WithTimeout(context.Background(), etcdTimeout) + defer cancel() + _, err = s.etcdClient.Delete(ctx, "/workloads/"+workloadID) + if err != nil { + return fmt.Errorf("failed to delete workload %s: %v", workloadID, err) + } + _, _ = s.etcdClient.Delete(ctx, assignmentKey(workloadID)) + _, _ = s.etcdClient.Delete(ctx, retryKey(workloadID)) + _, _ = s.etcdClient.Delete(ctx, reconciliationKey(workloadID)) + s.emitEvent("Rescheduled", workloadID, "", "Workload state removed", nil) + schedulerLogger.WithField("workload_id", workloadID).Info("deleted workload") + return nil +} + +// DeleteWorkload removes a workload from etcd. +func (s *Scheduler) DeleteWorkload(workloadID string) error { + return s.DeleteWorkloadWithContext(context.Background(), workloadID) +} + +// UpdateWorkloadStatus updates the status of a workload. +func (s *Scheduler) UpdateWorkloadStatus(workloadID, status string) error { + workload, err := s.GetWorkloadByID(workloadID) + if err != nil { + return err + } + + workload.Status = status + workload.StatusInfo.ActualState = status + workload.StatusInfo.LastUpdated = time.Now().UTC() + if workload.Metadata == nil { + workload.Metadata = map[string]interface{}{} + } + if strings.EqualFold(status, "failed") { + workload.Metadata["last_action"] = "Failed" + } + if err := s.saveWorkload(workload); err != nil { + return fmt.Errorf("failed to update workload %s status: %v", workloadID, err) + } + + schedulerLogger.WithFields(logrus.Fields{ + "workload_id": workloadID, + "status": status, + }).Debug("updated workload status") + return nil +} + +// UpdateWorkloadLogs updates the logs of a workload. +func (s *Scheduler) UpdateWorkloadLogs(workloadID, logs string) error { + workload, err := s.GetWorkloadByID(workloadID) + if err != nil { + return err + } + + // Append logs with timestamp + timestamp := time.Now().Format("2006-01-02 15:04:05") + logEntry := fmt.Sprintf("[%s] %s\n", timestamp, logs) + + if workload.Logs == "" { + workload.Logs = logEntry + } else { + workload.Logs += logEntry + } + workload.StatusInfo.LastUpdated = time.Now().UTC() + if err := s.saveWorkload(workload); err != nil { + return fmt.Errorf("failed to update workload %s logs: %v", workloadID, err) + } + + schedulerLogger.WithField("workload_id", workloadID).Debug("updated workload logs") + return nil +} + +// GetWorkloadsByNode retrieves all workloads assigned to a specific node. +func (s *Scheduler) GetWorkloadsByNode(nodeID string) ([]models.Workload, error) { + workloads, err := s.GetWorkloads() + if err != nil { + return nil, err + } + + nodeWorkloads := make([]models.Workload, 0) + for _, workload := range workloads { + if workload.NodeID == nodeID { + nodeWorkloads = append(nodeWorkloads, workload) + } + } + + return nodeWorkloads, nil +} + +// MonitorNodes periodically checks node health and updates status. +func (s *Scheduler) MonitorNodes(ctx context.Context) { + ticker := time.NewTicker(1 * time.Minute) + defer ticker.Stop() + for { + select { + case <-ctx.Done(): + schedulerLogger.Info("stopping node monitoring") + return + case <-ticker.C: + nodes, err := s.GetNodes() + if err != nil { + schedulerLogger.WithError(err).Error("node monitoring cycle failed") + continue + } + for _, node := range nodes { + if time.Since(node.LastHeartbeat) > 3*time.Minute { + reason := fmt.Sprintf("heartbeat expired: last heartbeat %s", node.LastHeartbeat.UTC().Format(time.RFC3339)) + if err := s.markNodeNotReady(node.NodeID, reason, "monitor"); err != nil { + schedulerLogger.WithError(err).WithField("node_id", node.NodeID).Warn("failed to update node status") + } + } + } + } + } +} + +// StartMonitoring starts both node monitoring and workload monitoring +func (s *Scheduler) StartMonitoring(ctx context.Context) { + // Start node monitoring + s.bgWG.Add(1) + go func() { + defer s.bgWG.Done() + s.MonitorNodes(ctx) + }() + + // Start workload monitoring + if s.monitor != nil { + s.bgWG.Add(1) + go func() { + defer s.bgWG.Done() + s.monitor.MonitorWorkloads(ctx, 60*time.Second) + }() + } +} + +// StartReconciliation starts the reconciliation loop +func (s *Scheduler) StartReconciliation(ctx context.Context) { + if s.reconciler != nil { + interval := schedulerDurationEnv("SCHEDULER_RECONCILE_INTERVAL", 5*time.Second) + s.bgWG.Add(1) + go func() { + defer s.bgWG.Done() + s.reconciler.StartReconciliationLoop(ctx, interval) + }() + } +} + +// WaitForBackground blocks until scheduler background workers stop or timeout elapses. +func (s *Scheduler) WaitForBackground(timeout time.Duration) bool { + done := make(chan struct{}) + go func() { + s.bgWG.Wait() + close(done) + }() + + select { + case <-done: + return true + case <-time.After(timeout): + return false + } +} + +// GetReconciliationStats returns reconciliation statistics +func (s *Scheduler) GetReconciliationStats() (map[string]interface{}, error) { + if s.reconciler != nil { + return s.reconciler.GetReconciliationStats() + } + return nil, fmt.Errorf("reconciler not initialized") +} + +// ReconcileAllWorkloads performs reconciliation on all workloads +func (s *Scheduler) ReconcileAllWorkloads() ([]*ReconciliationResult, error) { + if s.reconciler != nil { + return s.reconciler.ReconcileAllWorkloads(context.Background()) + } + return nil, fmt.Errorf("reconciler not initialized") +} + +// ReconcileWorkload performs reconciliation on a specific workload +func (s *Scheduler) ReconcileWorkload(workload models.Workload) (*ReconciliationResult, error) { + return s.ReconcileWorkloadWithContext(context.Background(), workload) +} + +// ReconcileWorkloadWithContext performs reconciliation with caller context propagation. +func (s *Scheduler) ReconcileWorkloadWithContext(ctx context.Context, workload models.Workload) (*ReconciliationResult, error) { + if s.reconciler != nil { + return s.reconciler.ReconcileWorkload(ctx, workload) + } + return nil, fmt.Errorf("reconciler not initialized") +} diff --git a/persys-scheduler/internal/scheduler/state_store.go b/persys-scheduler/internal/scheduler/state_store.go new file mode 100644 index 0000000..72d77de --- /dev/null +++ b/persys-scheduler/internal/scheduler/state_store.go @@ -0,0 +1,155 @@ +package scheduler + +import ( + "encoding/json" + "fmt" + "strings" + "time" + + "github.com/google/uuid" + "github.com/persys-dev/persys-cloud/persys-scheduler/internal/models" + clientv3 "go.etcd.io/etcd/client/v3" +) + +const ( + nodesPrefix = "/nodes/" + workloadsPrefix = "/workloads/" + assignmentsPrefix = "/assignments/" + reconciliationPrefix = "/reconciliation/" + retriesPrefix = "/retries/" + eventsPrefix = "/events/" +) + +func workloadKey(workloadID string) string { return workloadsPrefix + workloadID } +func assignmentKey(workloadID string) string { return assignmentsPrefix + workloadID } +func reconciliationKey(workloadID string) string { return reconciliationPrefix + workloadID } +func retryKey(workloadID string) string { return retriesPrefix + workloadID } +func eventKey(eventID string) string { return eventsPrefix + eventID } + +func defaultRetryState() models.RetryState { + return models.RetryState{ + Attempts: 0, + MaxAttempts: 5, + } +} + +func normalizeDesiredStateString(v string) string { + switch strings.ToLower(strings.TrimSpace(v)) { + case "stopped", "stop": + return "Stopped" + case "deleted", "delete": + return "Deleted" + default: + return "Running" + } +} + +func (s *Scheduler) initializeWorkloadDefaults(workload *models.Workload) { + if workload.DesiredState == "" { + workload.DesiredState = "Running" + } + workload.DesiredState = normalizeDesiredStateString(workload.DesiredState) + if workload.Status == "" { + workload.Status = "Pending" + } + if workload.CreatedAt.IsZero() { + workload.CreatedAt = time.Now().UTC() + } + if workload.Retry.MaxAttempts <= 0 { + workload.Retry = defaultRetryState() + } + if workload.Metadata == nil { + workload.Metadata = map[string]interface{}{} + } + if _, ok := workload.Metadata["created_at"]; !ok { + workload.Metadata["created_at"] = workload.CreatedAt.Format(time.RFC3339) + } + if _, ok := workload.Metadata["last_action"]; !ok { + workload.Metadata["last_action"] = "Created" + } + workload.StatusInfo.LastUpdated = time.Now().UTC() + if workload.StatusInfo.ActualState == "" { + workload.StatusInfo.ActualState = workload.Status + } +} + +func (s *Scheduler) saveWorkload(workload models.Workload) error { + payload, err := json.Marshal(workload) + if err != nil { + return fmt.Errorf("marshal workload %s: %w", workload.ID, err) + } + if err := s.RetryableEtcdPut(workloadKey(workload.ID), string(payload)); err != nil { + return err + } + retryPayload, err := json.Marshal(workload.Retry) + if err == nil { + _ = s.RetryableEtcdPut(retryKey(workload.ID), string(retryPayload)) + } + return nil +} + +func (s *Scheduler) writeAssignment(workloadID, nodeID, reason string) error { + rec := models.AssignmentRecord{ + WorkloadID: workloadID, + NodeID: nodeID, + Reason: reason, + CreatedAt: time.Now().UTC(), + } + payload, err := json.Marshal(rec) + if err != nil { + return err + } + return s.RetryableEtcdPut(assignmentKey(workloadID), string(payload)) +} + +func (s *Scheduler) writeReconciliationRecord(workloadID, action string, success bool, reason string) { + rec := models.ReconciliationRecord{ + WorkloadID: workloadID, + Action: action, + Success: success, + Reason: reason, + AttemptedAt: time.Now().UTC(), + } + payload, err := json.Marshal(rec) + if err != nil { + return + } + _ = s.RetryableEtcdPut(reconciliationKey(workloadID), string(payload)) +} + +func (s *Scheduler) emitEvent(eventType, workloadID, nodeID, reason string, details map[string]interface{}) { + event := models.SchedulerEvent{ + ID: uuid.NewString(), + Type: eventType, + WorkloadID: workloadID, + NodeID: nodeID, + Reason: reason, + Timestamp: time.Now().UTC(), + Details: details, + } + payload, err := json.Marshal(event) + if err != nil { + return + } + _ = s.RetryableEtcdPut(eventKey(event.ID), string(payload)) +} + +func (s *Scheduler) ListSchedulerEvents(limit int64) ([]models.SchedulerEvent, error) { + opts := []clientv3.OpOption{clientv3.WithPrefix()} + if limit > 0 { + opts = append(opts, clientv3.WithLimit(limit)) + } + resp, err := s.RetryableEtcdGet(eventsPrefix, opts...) + if err != nil { + return nil, err + } + events := make([]models.SchedulerEvent, 0, len(resp.Kvs)) + for _, kv := range resp.Kvs { + var event models.SchedulerEvent + if err := json.Unmarshal(kv.Value, &event); err != nil { + continue + } + events = append(events, event) + } + return events, nil +} diff --git a/persys-scheduler/internal/scheduler/workload_control.go b/persys-scheduler/internal/scheduler/workload_control.go new file mode 100644 index 0000000..c108f52 --- /dev/null +++ b/persys-scheduler/internal/scheduler/workload_control.go @@ -0,0 +1,221 @@ +package scheduler + +import ( + "encoding/json" + "fmt" + "strings" + "time" + + "github.com/google/uuid" + "github.com/persys-dev/persys-cloud/persys-scheduler/internal/models" +) + +func (s *Scheduler) CreateWorkload(workload models.Workload) (models.Workload, error) { + if strings.TrimSpace(workload.ID) == "" { + workload.ID = uuid.NewString() + } + s.ensureWorkloadRevision(&workload) + s.initializeWorkloadDefaults(&workload) + workload.Status = "Pending" + workload.StatusInfo.ActualState = "Pending" + workload.Metadata["last_action"] = "Created" + + if err := s.saveWorkload(workload); err != nil { + return models.Workload{}, err + } + s.emitEvent("WorkloadScheduled", workload.ID, "", "Workload created", map[string]interface{}{"desired_state": workload.DesiredState}) + return workload, nil +} + +func (s *Scheduler) UpdateWorkloadSpec(workloadID string, update models.Workload) (models.Workload, error) { + current, err := s.GetWorkloadByID(workloadID) + if err != nil { + return models.Workload{}, err + } + if current.Metadata == nil { + current.Metadata = map[string]interface{}{} + } + + if strings.TrimSpace(update.Name) != "" { + current.Name = update.Name + } + if strings.TrimSpace(update.Type) != "" { + current.Type = update.Type + } + if strings.TrimSpace(update.DesiredState) != "" { + current.DesiredState = normalizeDesiredStateString(update.DesiredState) + } + if strings.TrimSpace(update.Image) != "" { + current.Image = update.Image + } + if update.Command != "" { + current.Command = update.Command + } + if update.Compose != "" { + current.Compose = update.Compose + } + if update.ComposeYAML != "" { + current.ComposeYAML = update.ComposeYAML + } + if update.ProjectName != "" { + current.ProjectName = update.ProjectName + } + if len(update.EnvVars) > 0 { + current.EnvVars = update.EnvVars + } + if len(update.Labels) > 0 { + current.Labels = update.Labels + } + if len(update.Ports) > 0 { + current.Ports = update.Ports + } + if len(update.Volumes) > 0 { + current.Volumes = update.Volumes + } + if update.RestartPolicy != "" { + current.RestartPolicy = update.RestartPolicy + } + if update.VM != nil { + current.VM = update.VM + } + + current.RevisionID = "" + s.ensureWorkloadRevision(¤t) + current.Status = "Updating" + current.StatusInfo.ActualState = "Pending" + current.StatusInfo.LastUpdated = time.Now().UTC() + current.Metadata["last_action"] = "Updated" + + if err := s.saveWorkload(current); err != nil { + return models.Workload{}, err + } + s.emitEvent("WorkloadScheduled", current.ID, current.NodeID, "Workload updated", map[string]interface{}{"revision_id": current.RevisionID}) + return current, nil +} + +func (s *Scheduler) MarkWorkloadDeleted(workloadID string) error { + workload, err := s.GetWorkloadByID(workloadID) + if err != nil { + return err + } + if workload.Metadata == nil { + workload.Metadata = map[string]interface{}{} + } + workload.DesiredState = "Deleted" + workload.Status = "Deleting" + workload.StatusInfo.LastUpdated = time.Now().UTC() + workload.Metadata["last_action"] = "MarkedDeleted" + if err := s.saveWorkload(workload); err != nil { + return err + } + s.emitEvent("WorkloadScheduled", workload.ID, workload.NodeID, "Marked for deletion", nil) + return nil +} + +func (s *Scheduler) TriggerWorkloadRetry(workloadID string) (models.Workload, error) { + workload, err := s.GetWorkloadByID(workloadID) + if err != nil { + return models.Workload{}, err + } + if workload.Metadata == nil { + workload.Metadata = map[string]interface{}{} + } + if workload.Retry.MaxAttempts <= 0 { + workload.Retry.MaxAttempts = 5 + } + workload.Retry.NextRetryAt = time.Now().UTC() + workload.Metadata["last_action"] = "RetryTriggered" + if err := s.saveWorkload(workload); err != nil { + return models.Workload{}, err + } + s.emitEvent("RetryTriggered", workload.ID, workload.NodeID, "Manual retry requested", map[string]interface{}{"attempts": workload.Retry.Attempts}) + return workload, nil +} + +func (s *Scheduler) UpdateWorkloadRetryOnFailure(workloadID string, reason string) error { + workload, err := s.GetWorkloadByID(workloadID) + if err != nil { + return err + } + if workload.Metadata == nil { + workload.Metadata = map[string]interface{}{} + } + if workload.Retry.MaxAttempts <= 0 { + workload.Retry.MaxAttempts = 5 + } + workload.Retry.Attempts++ + if workload.Retry.Attempts >= workload.Retry.MaxAttempts { + workload.Status = "Failed" + workload.StatusInfo.ActualState = "Failed" + workload.StatusInfo.FailureReason = reason + workload.StatusInfo.LastUpdated = time.Now().UTC() + workload.Metadata["last_error"] = reason + workload.Metadata["last_action"] = "RetryExhausted" + s.emitEvent("WorkloadFailed", workload.ID, workload.NodeID, reason, map[string]interface{}{"attempts": workload.Retry.Attempts}) + return s.saveWorkload(workload) + } + + backoff := retryBackoff(workload.Retry.Attempts) + workload.Retry.NextRetryAt = time.Now().UTC().Add(backoff) + workload.Status = "RetryPending" + workload.StatusInfo.LastUpdated = time.Now().UTC() + workload.StatusInfo.FailureReason = reason + workload.Metadata["last_error"] = reason + workload.Metadata["last_action"] = "RetryScheduled" + s.emitEvent("RetryTriggered", workload.ID, workload.NodeID, reason, map[string]interface{}{"attempts": workload.Retry.Attempts, "next_retry_at": workload.Retry.NextRetryAt.Format(time.RFC3339)}) + return s.saveWorkload(workload) +} + +func retryBackoff(attempt int) time.Duration { + if attempt < 1 { + attempt = 1 + } + backoff := 5 * time.Second + for i := 1; i < attempt; i++ { + backoff *= 2 + } + if backoff > 2*time.Minute { + return 2 * time.Minute + } + return backoff +} + +func (s *Scheduler) dueForRetry(workload models.Workload) bool { + if workload.Retry.Attempts >= workload.Retry.MaxAttempts && workload.Retry.MaxAttempts > 0 { + return false + } + if workload.Retry.NextRetryAt.IsZero() { + return false + } + return !time.Now().UTC().Before(workload.Retry.NextRetryAt) +} + +func (s *Scheduler) DumpWorkloadRecord(workloadID string) (map[string]interface{}, error) { + workload, err := s.GetWorkloadByID(workloadID) + if err != nil { + return nil, err + } + result := map[string]interface{}{} + raw, err := json.Marshal(workload) + if err != nil { + return nil, err + } + if err := json.Unmarshal(raw, &result); err != nil { + return nil, err + } + return result, nil +} + +func (s *Scheduler) EnsureWorkloadAssigned(workload *models.Workload) error { + if workload.NodeID != "" { + return nil + } + node, reason, err := s.selectNodeForWorkload(*workload) + if err != nil { + return err + } + if err := s.assignWorkload(workload, node, reason); err != nil { + return fmt.Errorf("assign workload: %w", err) + } + return nil +} diff --git a/persys-scheduler/internal/telemetry/otel.go b/persys-scheduler/internal/telemetry/otel.go new file mode 100644 index 0000000..45a4646 --- /dev/null +++ b/persys-scheduler/internal/telemetry/otel.go @@ -0,0 +1,74 @@ +package telemetry + +import ( + "context" + "net/url" + "os" + "strings" + + "go.opentelemetry.io/otel" + "go.opentelemetry.io/otel/attribute" + "go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp" + "go.opentelemetry.io/otel/propagation" + "go.opentelemetry.io/otel/sdk/resource" + tracesdk "go.opentelemetry.io/otel/sdk/trace" +) + +func SetupOpenTelemetry(ctx context.Context, serviceName string) (func(context.Context) error, error) { + endpoint := strings.TrimSpace(os.Getenv("OTEL_EXPORTER_OTLP_ENDPOINT")) + if endpoint == "" { + endpoint = strings.TrimSpace(os.Getenv("JAEGER_ENDPOINT")) + } + if endpoint == "" { + endpoint = "jaeger:4318" + } + + opts := make([]otlptracehttp.Option, 0, 3) + if strings.HasPrefix(endpoint, "http://") || strings.HasPrefix(endpoint, "https://") { + u, err := url.Parse(endpoint) + if err != nil { + return nil, err + } + opts = append(opts, otlptracehttp.WithEndpoint(u.Host)) + if p := strings.TrimSpace(u.Path); p != "" && p != "/" { + opts = append(opts, otlptracehttp.WithURLPath(p)) + } + if u.Scheme != "https" { + opts = append(opts, otlptracehttp.WithInsecure()) + } + } else { + opts = append(opts, otlptracehttp.WithEndpoint(endpoint), otlptracehttp.WithInsecure()) + } + + if strings.EqualFold(strings.TrimSpace(os.Getenv("OTEL_EXPORTER_OTLP_INSECURE")), "true") { + opts = append(opts, otlptracehttp.WithInsecure()) + } + + exporter, err := otlptracehttp.New(ctx, opts...) + if err != nil { + return nil, err + } + + res, err := resource.Merge( + resource.Default(), + resource.NewSchemaless( + attribute.String("service.name", serviceName), + ), + ) + if err != nil { + return nil, err + } + + tracerProvider := tracesdk.NewTracerProvider( + tracesdk.WithBatcher(exporter), + tracesdk.WithResource(res), + ) + + otel.SetTracerProvider(tracerProvider) + otel.SetTextMapPropagator(propagation.NewCompositeTextMapPropagator( + propagation.TraceContext{}, + propagation.Baggage{}, + )) + + return tracerProvider.Shutdown, nil +} diff --git a/persys-scheduler/sample.env b/persys-scheduler/sample.env new file mode 100644 index 0000000..21e62f0 --- /dev/null +++ b/persys-scheduler/sample.env @@ -0,0 +1,25 @@ +# Persys Prow Scheduler Sample Environment Variables + +# ETCD endpoints (comma-separated) +ETCD_ENDPOINTS=etcd:2379 + +# Domain for service discovery +DOMAIN=cluster.local.dev + +# Scheduler ports +GRPC_PORT=8085 +METRICS_PORT=8084 +# For local testing without mTLS: +# ./persys-scheduler -insecure + +# Certificate management +CA_FILE=/etc/prow/certs/ca.pem +CFSSL_API_URL=https://persys-cfssl:8888 +CERT_COMMON_NAME=persys-scheduler +CERT_ORGANIZATION=persys + +# Tracing (OTLP) +# You can also set OTEL_EXPORTER_OTLP_ENDPOINT, which takes precedence. +JAEGER_ENDPOINT=jaeger:4318 +OTEL_EXPORTER_OTLP_ENDPOINT= +OTEL_EXPORTER_OTLP_INSECURE=true diff --git a/prow/README.md b/prow/README.md deleted file mode 100644 index e69de29..0000000 diff --git a/prow/cmd/scheduler/main.go b/prow/cmd/scheduler/main.go deleted file mode 100644 index 3637f4a..0000000 --- a/prow/cmd/scheduler/main.go +++ /dev/null @@ -1,259 +0,0 @@ -package main - -import ( - "context" - "crypto/tls" - "crypto/x509" - "log" - "net" - "net/http" - "os" - "os/signal" - "strconv" - "syscall" - "time" - - "github.com/gin-gonic/gin" - "github.com/persys-dev/prow/internal/api" - "github.com/persys-dev/prow/internal/auth" - "github.com/persys-dev/prow/internal/middleware" - "github.com/persys-dev/prow/internal/scheduler" - ginprometheus "github.com/zsais/go-gin-prometheus" - gootelgin "go.opentelemetry.io/contrib/instrumentation/github.com/gin-gonic/gin/otelgin" - "go.opentelemetry.io/otel" - "go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp" - "go.opentelemetry.io/otel/sdk/resource" - "go.opentelemetry.io/otel/sdk/trace" - semconv "go.opentelemetry.io/otel/semconv/v1.17.0" -) - -// Tracing setup function -func setupTracer() func() { - exporter, err := otlptracehttp.New(context.Background(), - otlptracehttp.WithEndpoint("192.168.1.13:4318"), - otlptracehttp.WithInsecure(), - ) - if err != nil { - log.Printf("Failed to create OTLP exporter: %v", err) - return func() {} - } - - tp := trace.NewTracerProvider( - trace.WithBatcher(exporter), - trace.WithResource(resource.NewWithAttributes( - semconv.SchemaURL, - semconv.ServiceNameKey.String("prow-scheduler"), - )), - ) - otel.SetTracerProvider(tp) - return func() { _ = tp.Shutdown(context.Background()) } -} - -func main() { - // Setup tracing - shutdown := setupTracer() - defer shutdown() - - // Get configuration from environment - caFile := os.Getenv("CA_FILE") - cfsslURL := os.Getenv("CFSSL_API_URL") - commonName := os.Getenv("CERT_COMMON_NAME") - organization := os.Getenv("CERT_ORGANIZATION") - - // Set defaults if not provided - if caFile == "" { - caFile = "/etc/prow/certs/ca.pem" - } - if cfsslURL == "" { - cfsslURL = "https://persys-cfssl:8888" - } - if commonName == "" { - commonName = "prow-scheduler" - } - if organization == "" { - organization = "persys" - } - - // Log the paths we're using - log.Printf("Using CA certificate path: %s", caFile) - - // Initialize certificate manager - certManager := auth.NewCertificateManager(caFile, cfsslURL, commonName, organization) - - log.Printf("bootstrapping prow-scheduler ...") - time.Sleep(15 * time.Second) - - // Ensure we have valid certificates - if err := certManager.EnsureCertificate(); err != nil { - log.Fatalf("Failed to ensure certificate: %v", err) - } - - // Load certificates for mTLS - cert, err := tls.LoadX509KeyPair(certManager.CertPath, certManager.KeyPath) - if err != nil { - log.Fatalf("Failed to load certificate pair: %v", err) - } - - // Load CA certificate - caCert, err := os.ReadFile(caFile) - if err != nil { - log.Fatalf("Failed to read CA cert: %v , %s", caFile, err) - } - caCertPool := x509.NewCertPool() - if !caCertPool.AppendCertsFromPEM(caCert) { - log.Fatalf("Failed to append CA cert to pool") - } - - // Initialize scheduler - sched, err := scheduler.NewScheduler() - if err != nil { - log.Fatalf("Failed to initialize scheduler: %v", err) - } - defer sched.Close() - - // Get scheduler's IP address for CoreDNS registration - schedulerIP := os.Getenv("SCHEDULER_IP") - if schedulerIP == "" { - // Try to get IP from hostname if not provided - hostname, err := os.Hostname() - if err != nil { - log.Printf("Warning: Failed to get hostname: %v", err) - } else { - // Resolve hostname to IP - addrs, err := net.LookupHost(hostname) - if err != nil || len(addrs) == 0 { - log.Printf("Warning: Failed to resolve hostname %s: %v", hostname, err) - } else { - schedulerIP = addrs[0] - } - } - } - - // Get port for CoreDNS registration - schedulerPort := 8085 // Default port - if portStr := os.Getenv("PORT"); portStr != "" { - if port, err := strconv.Atoi(portStr); err == nil { - schedulerPort = port - } - } - - if schedulerIP != "" { - // Register scheduler in CoreDNS - if err := sched.RegisterSchedulerInCoreDNS(schedulerIP, schedulerPort); err != nil { - log.Printf("Warning: Failed to register scheduler in CoreDNS: %v", err) - } else { - log.Printf("Successfully registered scheduler in CoreDNS with IP: %s and port: %d", schedulerIP, schedulerPort) - } - } else { - log.Printf("Warning: Could not determine scheduler IP for CoreDNS registration") - } - - // Set up context for graceful shutdown - ctx, cancel := context.WithCancel(context.Background()) - defer cancel() - - // Start node monitoring - go sched.MonitorNodes(ctx) - - // Start Workload Monitoring - workloadMonitor := scheduler.NewMonitor(sched) - go workloadMonitor.StartMonitoring() - - // Start reconciliation loop - log.Println("Starting reconciliation loop...") - sched.StartReconciliation(ctx) - - // Set up Gin router - r := gin.New() - r.Use(gin.Logger()) - r.Use(gin.Recovery()) - - // Enable OpenTelemetry tracing - r.Use(gootelgin.Middleware("prow-scheduler")) - r.Use(middleware.ServiceIdentityHeader("prow-scheduler")) - - // Enable Prometheus metrics - p := ginprometheus.NewPrometheus("prow_scheduler") - p.Use(r) - - // Create a separate router for non-mTLS endpoints - nonMTLSRouter := gin.New() - nonMTLSRouter.Use(gin.Logger()) - nonMTLSRouter.Use(gin.Recovery()) - - // Enable OpenTelemetry tracing for non-mTLS router - nonMTLSRouter.Use(gootelgin.Middleware("prow_scheduler-non-mtls")) - nonMTLSRouter.Use(middleware.ServiceIdentityHeader("prow-scheduler")) - - // Enable Prometheus metrics on non-mTLS router - p.Use(nonMTLSRouter) - - // Register API handlers for both routers - api.RegisterMTLSHandlers(r, sched) - api.RegisterNonMTLSHandlers(nonMTLSRouter, sched) - - // Start server - port := os.Getenv("PORT") - if port == "" { - port = "8085" - } - - // Configure TLS - tlsConfig := &tls.Config{ - Certificates: []tls.Certificate{cert}, - ClientCAs: caCertPool, - ClientAuth: tls.RequireAndVerifyClientCert, - MinVersion: tls.VersionTLS12, - } - - // Create mTLS server - mtlsServer := &http.Server{ - Addr: ":" + port, - Handler: r, - TLSConfig: tlsConfig, - } - - // Create non-mTLS server for metrics and nodes - nonMTLSPort := "8084" // Use a different port for non-mTLS endpoints - nonMTLSServer := &http.Server{ - Addr: ":" + nonMTLSPort, - Handler: nonMTLSRouter, - } - - // Start mTLS server in a goroutine - go func() { - log.Printf("Starting mTLS server on port %s", port) - if err := mtlsServer.ListenAndServeTLS(certManager.CertPath, certManager.KeyPath); err != nil && err != http.ErrServerClosed { - log.Fatalf("Failed to start mTLS server: %v", err) - } - }() - - // Start non-mTLS server in a goroutine - go func() { - log.Printf("Starting non-mTLS server on port %s", nonMTLSPort) - if err := nonMTLSServer.ListenAndServe(); err != nil && err != http.ErrServerClosed { - log.Fatalf("Failed to start non-mTLS server: %v", err) - } - }() - - // Handle shutdown signals - sigChan := make(chan os.Signal, 1) - signal.Notify(sigChan, syscall.SIGINT, syscall.SIGTERM) - <-sigChan - - // Perform graceful shutdown - log.Println("Shutting down servers...") - shutdownCtx, shutdownCancel := context.WithTimeout(context.Background(), 5*time.Second) - defer shutdownCancel() - - if err := mtlsServer.Shutdown(shutdownCtx); err != nil { - log.Printf("mTLS server shutdown error: %v", err) - } - - if err := nonMTLSServer.Shutdown(shutdownCtx); err != nil { - log.Printf("non-mTLS server shutdown error: %v", err) - } - - cancel() // Stop MonitorNodes - log.Println("Servers stopped") -} diff --git a/prow/go.mod b/prow/go.mod deleted file mode 100644 index 5e46c32..0000000 --- a/prow/go.mod +++ /dev/null @@ -1,77 +0,0 @@ -module github.com/persys-dev/prow - -go 1.24.1 - -require ( - github.com/gin-gonic/gin v1.10.1 - github.com/google/uuid v1.6.0 - github.com/zsais/go-gin-prometheus v0.1.0 - go.opentelemetry.io/contrib/instrumentation/github.com/gin-gonic/gin/otelgin v0.61.0 - go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.46.1 - go.opentelemetry.io/otel v1.36.0 - go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.36.0 - go.opentelemetry.io/otel/sdk v1.36.0 -) - -require ( - github.com/beorn7/perks v1.0.1 // indirect - github.com/cenkalti/backoff/v5 v5.0.2 // indirect - github.com/cespare/xxhash/v2 v2.3.0 // indirect - github.com/coreos/go-semver v0.3.0 // indirect - github.com/coreos/go-systemd/v22 v22.3.2 // indirect - github.com/felixge/httpsnoop v1.0.4 // indirect - github.com/go-logr/logr v1.4.2 // indirect - github.com/go-logr/stdr v1.2.2 // indirect - github.com/gogo/protobuf v1.3.2 // indirect - github.com/golang/protobuf v1.5.4 // indirect - github.com/grpc-ecosystem/grpc-gateway/v2 v2.26.3 // indirect - github.com/konsorten/go-windows-terminal-sequences v1.0.3 // indirect - github.com/matttproud/golang_protobuf_extensions v1.0.1 // indirect - github.com/prometheus/client_golang v1.11.1 // indirect - github.com/prometheus/client_model v0.2.0 // indirect - github.com/prometheus/common v0.26.0 // indirect - github.com/prometheus/procfs v0.6.0 // indirect - github.com/sirupsen/logrus v1.6.0 // indirect - go.etcd.io/etcd/api/v3 v3.5.21 // indirect - go.etcd.io/etcd/client/pkg/v3 v3.5.21 // indirect - go.opentelemetry.io/auto/sdk v1.1.0 // indirect - go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.36.0 // indirect - go.opentelemetry.io/otel/metric v1.36.0 // indirect - go.opentelemetry.io/otel/trace v1.36.0 // indirect - go.opentelemetry.io/proto/otlp v1.6.0 // indirect - go.uber.org/atomic v1.7.0 // indirect - go.uber.org/multierr v1.6.0 // indirect - go.uber.org/zap v1.17.0 // indirect - google.golang.org/genproto/googleapis/api v0.0.0-20250519155744-55703ea1f237 // indirect - google.golang.org/genproto/googleapis/rpc v0.0.0-20250519155744-55703ea1f237 // indirect - google.golang.org/grpc v1.72.1 // indirect -) - -require ( - github.com/bytedance/sonic v1.13.2 // indirect - github.com/bytedance/sonic/loader v0.2.4 // indirect - github.com/cloudwego/base64x v0.1.5 // indirect - github.com/gabriel-vasile/mimetype v1.4.9 // indirect - github.com/gin-contrib/sse v1.1.0 // indirect - github.com/go-playground/locales v0.14.1 // indirect - github.com/go-playground/universal-translator v0.18.1 // indirect - github.com/go-playground/validator/v10 v10.26.0 // indirect - github.com/goccy/go-json v0.10.5 // indirect - github.com/json-iterator/go v1.1.12 // indirect - github.com/klauspost/cpuid/v2 v2.2.10 // indirect - github.com/leodido/go-urn v1.4.0 // indirect - github.com/mattn/go-isatty v0.0.20 // indirect - github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect - github.com/modern-go/reflect2 v1.0.2 // indirect - github.com/pelletier/go-toml/v2 v2.2.4 // indirect - github.com/twitchyliquid64/golang-asm v0.15.1 // indirect - github.com/ugorji/go/codec v1.2.12 // indirect - go.etcd.io/etcd/client/v3 v3.5.21 - golang.org/x/arch v0.17.0 // indirect - golang.org/x/crypto v0.38.0 // indirect - golang.org/x/net v0.40.0 // indirect - golang.org/x/sys v0.33.0 // indirect - golang.org/x/text v0.25.0 // indirect - google.golang.org/protobuf v1.36.6 // indirect - gopkg.in/yaml.v3 v3.0.1 // indirect -) diff --git a/prow/internal/api/handlers.go b/prow/internal/api/handlers.go deleted file mode 100644 index bdf98a7..0000000 --- a/prow/internal/api/handlers.go +++ /dev/null @@ -1,99 +0,0 @@ -package api - -import ( - "encoding/json" - "log" - "net/http" - - "github.com/gin-gonic/gin" - "github.com/persys-dev/prow/internal/models" - "github.com/persys-dev/prow/internal/scheduler" - clientv3 "go.etcd.io/etcd/client/v3" -) - -// NodeHeartbeat represents a heartbeat message from a node -type NodeHeartbeat struct { - NodeID string `json:"nodeId"` - Status string `json:"status"` - AvailableCPU float64 `json:"availableCpu"` - AvailableMemory int64 `json:"availableMemory"` -} - -func RegisterMTLSHandlers(r *gin.Engine, sched *scheduler.Scheduler) { - // mTLS protected routes - mtls := r.Group("/") - { - // Schedule workload - mtls.POST("/workloads/schedule", func(c *gin.Context) { - var workload models.Workload - if err := c.ShouldBindJSON(&workload); err != nil { - c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()}) - return - } - workload.Status = "Pending" - - nodeID, err := sched.ScheduleWorkload(workload) - if err != nil { - log.Printf("Failed to schedule workload %s: %v", workload.ID, err) - c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()}) - return - } - c.JSON(http.StatusOK, gin.H{"node_id": nodeID}) - }) - - // List nodes - mtls.GET("/nodes", func(c *gin.Context) { - nodes, err := sched.GetNodes() - if err != nil { - c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to get nodes"}) - return - } - c.JSON(http.StatusOK, gin.H{"nodes": nodes}) - }) - - // List workloads - mtls.GET("/workloads", func(c *gin.Context) { - resp, err := sched.RetryableEtcdGet("/workloads/", clientv3.WithPrefix()) - if err != nil { - log.Printf("Failed to get workloads: %v", err) - c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to get workloads"}) - return - } - - var workloads []models.Workload - for _, kv := range resp.Kvs { - var workload models.Workload - if err := json.Unmarshal(kv.Value, &workload); err != nil { - continue - } - workloads = append(workloads, workload) - } - c.JSON(http.StatusOK, workloads) - }) - - // Cluster metrics - mtls.GET("/cluster/metrics", func(c *gin.Context) { - // Node metrics - nodes, err := sched.GetNodes() - if err != nil { - log.Printf("Failed to get nodes for metrics: %v", err) - c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to get nodes"}) - return - } - - // Workload metrics - resp, err := sched.RetryableEtcdGet("/workloads/") - if err != nil { - log.Printf("Failed to get workloads for metrics: %v", err) - c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to get workloads"}) - return - } - - c.JSON(http.StatusOK, gin.H{ - "nodes": nodes, - "workloads": resp.Kvs, - }) - }) - - } -} diff --git a/prow/internal/api/nonmtls_handlers.go b/prow/internal/api/nonmtls_handlers.go deleted file mode 100644 index 6bcbae2..0000000 --- a/prow/internal/api/nonmtls_handlers.go +++ /dev/null @@ -1,93 +0,0 @@ -package api - -import ( - "encoding/json" - "log" - "net/http" - "time" - - "github.com/gin-gonic/gin" - "github.com/persys-dev/prow/internal/models" - "github.com/persys-dev/prow/internal/scheduler" -) - -func RegisterNonMTLSHandlers(r *gin.Engine, sched *scheduler.Scheduler) { - r.POST("/nodes/register", func(c *gin.Context) { - var node models.Node - if err := c.ShouldBindJSON(&node); err != nil { - c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()}) - return - } - // Check if node already exists - _, err := sched.RetryableEtcdGet("/nodes/" + node.NodeID) - if err != nil { - c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to get nodes from etcd:", "err": err.Error()}) - return - } - if err := sched.RegisterNode(node); err != nil { - c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to register node:", "err": err.Error()}) - return - } - c.JSON(http.StatusOK, gin.H{"nodeId": node.NodeID, "status": "Registered"}) - }) - - r.POST("/nodes/heartbeat", func(c *gin.Context) { - var heartbeat struct { - NodeID string `json:"nodeId"` - Status string `json:"status"` - AvailableCPU float64 `json:"availableCpu"` - AvailableMemory int64 `json:"availableMemory"` - } - if err := c.ShouldBindJSON(&heartbeat); err != nil { - c.JSON(400, gin.H{"error": err.Error()}) - return - } - // Log only on error or periodically to reduce log volume - log.Printf("Received heartbeat for node %s (Status: %s)", heartbeat.NodeID, heartbeat.Status) - - resp, err := sched.RetryableEtcdGet("/nodes/" + heartbeat.NodeID) - if err != nil { - log.Printf("Failed to get node %s from etcd: %v", heartbeat.NodeID, err) - c.JSON(500, gin.H{"error": "Internal server error"}) - return - } - if resp == nil || len(resp.Kvs) == 0 { - log.Printf("Node %s not found in etcd", heartbeat.NodeID) - c.JSON(404, gin.H{"error": "Node not found"}) - return - } - - var node models.Node - if err := json.Unmarshal(resp.Kvs[0].Value, &node); err != nil { - log.Printf("Failed to unmarshal node %s: %v", heartbeat.NodeID, err) - c.JSON(500, gin.H{"error": "Internal server error"}) - return - } - - node.LastHeartbeat = time.Now() - if heartbeat.Status != "" { - node.Status = heartbeat.Status - } - if heartbeat.AvailableCPU > 0 { - node.AvailableCPU = heartbeat.AvailableCPU - } - if heartbeat.AvailableMemory > 0 { - node.AvailableMemory = heartbeat.AvailableMemory - } - - updatedNodeJSON, err := json.Marshal(node) - if err != nil { - log.Printf("Failed to marshal node %s: %v", heartbeat.NodeID, err) - c.JSON(500, gin.H{"error": "Internal server error"}) - return - } - - if err := sched.RetryableEtcdPut("/nodes/"+heartbeat.NodeID, string(updatedNodeJSON)); err != nil { - log.Printf("Failed to update node %s heartbeat: %v", heartbeat.NodeID, err) - c.JSON(500, gin.H{"error": "Failed to update heartbeat"}) - return - } - - c.JSON(200, gin.H{"message": "Heartbeat received"}) - }) -} diff --git a/prow/internal/api/reconciliation_handlers.go b/prow/internal/api/reconciliation_handlers.go deleted file mode 100644 index faaabc8..0000000 --- a/prow/internal/api/reconciliation_handlers.go +++ /dev/null @@ -1,201 +0,0 @@ -package api - -import ( - "encoding/json" - "net/http" - "time" - - "github.com/gin-gonic/gin" - "github.com/persys-dev/prow/internal/scheduler" -) - -// ReconciliationHandlers handles reconciliation-related API endpoints -type ReconciliationHandlers struct { - scheduler *scheduler.Scheduler -} - -// NewReconciliationHandlers creates a new ReconciliationHandlers instance -func NewReconciliationHandlers(scheduler *scheduler.Scheduler) *ReconciliationHandlers { - return &ReconciliationHandlers{ - scheduler: scheduler, - } -} - -// GetReconciliationStats returns reconciliation statistics -func (h *ReconciliationHandlers) GetReconciliationStats(c *gin.Context) { - stats, err := h.scheduler.GetReconciliationStats() - if err != nil { - c.JSON(http.StatusInternalServerError, gin.H{ - "error": "Failed to get reconciliation stats", - "message": err.Error(), - }) - return - } - - c.JSON(http.StatusOK, gin.H{ - "stats": stats, - "timestamp": time.Now().Format(time.RFC3339), - }) -} - -// TriggerReconciliation manually triggers a reconciliation cycle -func (h *ReconciliationHandlers) TriggerReconciliation(c *gin.Context) { - // Get all workloads - workloads, err := h.scheduler.GetWorkloads() - if err != nil { - c.JSON(http.StatusInternalServerError, gin.H{ - "error": "Failed to get workloads", - "message": err.Error(), - }) - return - } - - // Perform reconciliation - results, err := h.scheduler.ReconcileAllWorkloads() - if err != nil { - c.JSON(http.StatusInternalServerError, gin.H{ - "error": "Failed to perform reconciliation", - "message": err.Error(), - }) - return - } - - // Count results - successCount := 0 - actionCount := 0 - for _, result := range results { - if result.Success { - successCount++ - } - if result.Action != "NoAction" { - actionCount++ - } - } - - c.JSON(http.StatusOK, gin.H{ - "message": "Reconciliation triggered successfully", - "results": gin.H{ - "totalWorkloads": len(workloads), - "processed": len(results), - "actionsTaken": actionCount, - "successful": successCount, - "timestamp": time.Now().Format(time.RFC3339), - }, - }) -} - -// ReconcileWorkload manually reconciles a specific workload -func (h *ReconciliationHandlers) ReconcileWorkload(c *gin.Context) { - workloadID := c.Param("id") - if workloadID == "" { - c.JSON(http.StatusBadRequest, gin.H{ - "error": "Workload ID is required", - }) - return - } - - // Get the workload - workload, err := h.scheduler.GetWorkloadByID(workloadID) - if err != nil { - c.JSON(http.StatusNotFound, gin.H{ - "error": "Workload not found", - "message": err.Error(), - }) - return - } - - // Perform reconciliation - result, err := h.scheduler.ReconcileWorkload(workload) - if err != nil { - c.JSON(http.StatusInternalServerError, gin.H{ - "error": "Failed to reconcile workload", - "message": err.Error(), - }) - return - } - - c.JSON(http.StatusOK, gin.H{ - "message": "Workload reconciliation completed", - "result": result, - "timestamp": time.Now().Format(time.RFC3339), - }) -} - -// UpdateWorkloadDesiredState updates the desired state of a workload -func (h *ReconciliationHandlers) UpdateWorkloadDesiredState(c *gin.Context) { - workloadID := c.Param("id") - if workloadID == "" { - c.JSON(http.StatusBadRequest, gin.H{ - "error": "Workload ID is required", - }) - return - } - - var request struct { - DesiredState string `json:"desiredState" binding:"required"` - } - - if err := c.ShouldBindJSON(&request); err != nil { - c.JSON(http.StatusBadRequest, gin.H{ - "error": "Invalid request body", - "message": err.Error(), - }) - return - } - - // Validate desired state - validStates := map[string]bool{ - "Running": true, - "Stopped": true, - "Paused": true, - } - - if !validStates[request.DesiredState] { - c.JSON(http.StatusBadRequest, gin.H{ - "error": "Invalid desired state. Must be one of: Running, Stopped, Paused", - }) - return - } - - // Get the workload - workload, err := h.scheduler.GetWorkloadByID(workloadID) - if err != nil { - c.JSON(http.StatusNotFound, gin.H{ - "error": "Workload not found", - "message": err.Error(), - }) - return - } - - // Update desired state - workload.DesiredState = request.DesiredState - - // Save updated workload - workloadJSON, err := json.Marshal(workload) - if err != nil { - c.JSON(http.StatusInternalServerError, gin.H{ - "error": "Failed to marshal workload", - "message": err.Error(), - }) - return - } - - if err := h.scheduler.RetryableEtcdPut("/workloads/"+workloadID, string(workloadJSON)); err != nil { - c.JSON(http.StatusInternalServerError, gin.H{ - "error": "Failed to update workload", - "message": err.Error(), - }) - return - } - - c.JSON(http.StatusOK, gin.H{ - "message": "Workload desired state updated successfully", - "workload": gin.H{ - "id": workload.ID, - "name": workload.Name, - "desiredState": workload.DesiredState, - "currentStatus": workload.Status, - }, - "timestamp": time.Now().Format(time.RFC3339), - }) -} diff --git a/prow/internal/middleware/identity.go b/prow/internal/middleware/identity.go deleted file mode 100644 index 381989d..0000000 --- a/prow/internal/middleware/identity.go +++ /dev/null @@ -1,10 +0,0 @@ -package middleware - -import "github.com/gin-gonic/gin" - -func ServiceIdentityHeader(serviceName string) gin.HandlerFunc { - return func(c *gin.Context) { - c.Writer.Header().Set("X-Service-Name", serviceName) - c.Next() - } -} diff --git a/prow/internal/models/auth.go b/prow/internal/models/auth.go deleted file mode 100644 index 2718bdb..0000000 --- a/prow/internal/models/auth.go +++ /dev/null @@ -1,7 +0,0 @@ -package models - -// AuthConfig holds authentication-related configuration for nodes. -type AuthConfig struct { - PublicKey string `json:"publicKey"` // Hex-encoded RSA public key - SharedSecret string `json:"sharedSecret"` // Optional shared secret for Secret Mode -} \ No newline at end of file diff --git a/prow/internal/models/models.go b/prow/internal/models/models.go deleted file mode 100644 index 23d6ca7..0000000 --- a/prow/internal/models/models.go +++ /dev/null @@ -1,105 +0,0 @@ -package models - -import ( - "net/http" - "sync" - "time" - - clientv3 "go.etcd.io/etcd/client/v3" -) - -// Hypervisor represents virtualization technology details -type Hypervisor struct { - Type string `json:"type" binding:"required"` - Status string `json:"status" binding:"required"` - Version string `json:"version,omitempty"` -} - -// ContainerEngine represents container runtime details -type ContainerEngine struct { - Type string `json:"type" binding:"required"` - Status string `json:"status" binding:"required"` - Version string `json:"version,omitempty"` -} - -// Resources represents system resource usage -type Resources struct { - CPUUsage float64 `json:"cpu_usage,omitempty"` - MemoryUsage float64 `json:"memory_usage,omitempty"` - DiskUsage int `json:"disk_usage,omitempty"` -} - -// DockerSwarm represents swarm-specific information -type DockerSwarm struct { - Active bool `json:"active"` - NodeID string `json:"nodeId,omitempty"` - Role string `json:"role,omitempty"` - Status string `json:"status,omitempty"` - ManagerAddress string `json:"managerAddress,omitempty"` -} - -// Node represents a registered node -type Node struct { - NodeID string `json:"nodeId" binding:"required"` - IPAddress string `json:"ipAddress" binding:"required"` - Username string `json:"username" binding:"required"` - Hostname string `json:"hostname" binding:"required"` - OSName string `json:"osName" binding:"required"` - KernelVersion string `json:"kernelVersion" binding:"required"` - Status string `json:"status" binding:"required"` - Timestamp string `json:"timestamp" binding:"required"` - Resources Resources `json:"resources"` - TotalCPU float64 `json:"totalCpu"` - TotalMemory int64 `json:"totalMemory"` - AvailableCPU float64 `json:"availableCpu"` - AvailableMemory int64 `json:"availableMemory"` - Hypervisor Hypervisor `json:"hypervisor" binding:"required"` - ContainerEngine ContainerEngine `json:"containerEngine" binding:"required"` - DockerSwarm DockerSwarm `json:"dockerSwarm"` - LastHeartbeat time.Time `json:"lastHeartbeat"` - Labels map[string]string `json:"labels,omitempty"` - AgentPort int `json:"agentPort"` // Added for agent communication - DomainName string `json:"domainName,omitempty"` // Added field - AuthConfig AuthConfig `json:"authConfig"` // Authentication configuration -} - -// Workload represents a scheduled task -type Workload struct { - ID string `json:"id,omitempty"` - Name string `json:"name" binding:"required"` - Type string `json:"type" binding:"required"` // "docker-container", "docker-compose", "git-compose" - Image string `json:"image" binding:"required"` // For docker-container - Command string `json:"command,omitempty"` // For docker-container - Compose string `json:"compose,omitempty"` // Base64-encoded Compose content (optional) - GitRepo string `json:"gitRepo,omitempty"` // Git URL for git-compose - GitBranch string `json:"gitBranch,omitempty"` // Git branch for git-compose - GitToken string `json:"gitToken,omitempty"` // Optional Git auth token - EnvVars map[string]string `json:"envVars,omitempty"` // Environment variables - Resources Resources `json:"resources"` - NodeID string `json:"nodeId,omitempty"` - Status string `json:"status"` - DesiredState string `json:"desiredState,omitempty"` // Desired state for reconciliation - Labels map[string]string `json:"labels,omitempty"` - CreatedAt time.Time `json:"createdAt"` - LocalPath string `json:"localPath,omitempty"` // Local path for docker-compose - Ports []string `json:"ports,omitempty"` // e.g., ["8080:80"] - Volumes []string `json:"volumes,omitempty"` // e.g., ["/host:/container"] - Network string `json:"network,omitempty"` // e.g., "bridge" - RestartPolicy string `json:"restartPolicy,omitempty"` // e.g., "always" - Logs string `json:"logs,omitempty"` // Execution logs and output - Metadata map[string]interface{} `json:"metadata,omitempty"` // Reconciliation metadata -} - -// AgentCommand represents a command payload for the agent API -type AgentCommand struct { - Command string `json:"command"` - Image string `json:"image,omitempty"` -} - -// Scheduler manages nodes and workloads -type Scheduler struct { - etcdClient *clientv3.Client - mu sync.Mutex - httpClient *http.Client - domain string // Added domain field for DNS -} diff --git a/prow/internal/scheduler/milx.txt b/prow/internal/scheduler/milx.txt deleted file mode 100644 index 3eeb57e..0000000 --- a/prow/internal/scheduler/milx.txt +++ /dev/null @@ -1,53 +0,0 @@ -// SendCommandToNode sends a signed request to the node's agent API -func (s *Scheduler) SendCommandToNode(node models.Node, endpoint string, payload interface{}) (string, error) { - if node.IPAddress == "" || node.AgentPort == 0 { - return "", fmt.Errorf("node IPAddress and AgentPort are required") - } - - url := fmt.Sprintf("http://%s:%d%s", node.IPAddress, node.AgentPort, endpoint) - var req *http.Request - var err error - var signatureB64 string - - if payload != nil { - // Handle POST request - payloadBytes, err := json.Marshal(payload) - if err != nil { - return "", fmt.Errorf("failed to marshal payload: %v", err) - } - log.Printf("Sending POST command to %s with payload: %s", url, string(payloadBytes)) - - // Sign payload - hash := sha256.Sum256(payloadBytes) - signature, err := rsa.SignPKCS1v15(rand.Reader, s.privateKey, crypto.SHA256, hash[:]) - if err != nil { - return "", fmt.Errorf("failed to sign payload: %v", err) - } - signatureB64 = base64.StdEncoding.EncodeToString(signature) - log.Printf("Signature for node %s: %s...", node.NodeID, signatureB64[:50]) - - // Create POST request - req, err = http.NewRequest("POST", url, bytes.NewBuffer(payloadBytes)) - if err != nil { - return "", fmt.Errorf("failed to create POST request: %v", err) - } - req.Header.Set("Content-Type", "application/json") - } else { - // Handle GET request - log.Printf("Sending GET command to %s", url) - - // Sign an empty payload or URL for GET request - hash := sha256.Sum256([]byte(url)) // Sign the URL for GET requests - signature, err := rsa.SignPKCS1v15(rand.Reader, s.privateKey, crypto.SHA256, hash[:]) - if err != nil { - return "", fmt.Errorf("failed to sign URL for GET request: %v", err) - } - signatureB64 = base64.StdEncoding.EncodeToString(signature) - log.Printf("Signature for node %s: %s...", node.NodeID, signatureB64[:50]) - - // Create GET request - req, err = http.NewRequest("GET", url, nil) - if err != nil { - return "", fmt.Errorf("failed to create GET request: %v", err) - } - } \ No newline at end of file diff --git a/prow/internal/scheduler/monitor.go b/prow/internal/scheduler/monitor.go deleted file mode 100644 index ca6e44c..0000000 --- a/prow/internal/scheduler/monitor.go +++ /dev/null @@ -1,208 +0,0 @@ -package scheduler - -import ( - "context" - "encoding/json" - "fmt" - "log" - "strings" - "time" - - "github.com/persys-dev/prow/internal/models" -) - -// Monitor handles container status monitoring -type Monitor struct { - scheduler *Scheduler -} - -// ContainerInfo represents a container's info from /docker/list -// Add Reason field for detailed status -type ContainerInfo struct { - ID string `json:"id"` - Name string `json:"names"` - Status string `json:"status"` - Reason string `json:"reason,omitempty"` -} - -// ContainerLogs represents container logs from /docker/logs -type ContainerLogs struct { - ContainerID string `json:"containerId"` - Logs string `json:"logs"` -} - -// dockerListResponse matches the /docker/list JSON structure -type dockerListResponse struct { - Result []ContainerInfo `json:"result"` -} - -// NewMonitor creates a new Monitor instance -func NewMonitor(scheduler *Scheduler) *Monitor { - return &Monitor{scheduler: scheduler} -} - -// getContainerList queries the node's /docker/list endpoint -func (m *Monitor) getContainerList(node models.Node) ([]ContainerInfo, error) { - endpoint := "/docker/list" - response, err := m.scheduler.SendCommandToNode(node, endpoint, nil) - if err != nil { - return nil, fmt.Errorf("failed to query %s on node %s: %v", endpoint, node.NodeID, err) - } - - log.Printf("Raw /docker/list response from node %s: %s", node.NodeID, response) - - var dockerResp dockerListResponse - if err := json.Unmarshal([]byte(response), &dockerResp); err != nil { - return nil, fmt.Errorf("failed to decode %s response from node %s: %v", endpoint, node.NodeID, err) - } - - log.Printf("Retrieved %d containers from node %s", len(dockerResp.Result), node.NodeID) - return dockerResp.Result, nil -} - -// getContainerLogs queries the node's /docker/logs endpoint for a specific container -func (m *Monitor) getContainerLogs(node models.Node, containerID string) (string, error) { - endpoint := fmt.Sprintf("/docker/logs/%s", containerID) - response, err := m.scheduler.SendCommandToNode(node, endpoint, nil) - if err != nil { - return "", fmt.Errorf("failed to query %s on node %s: %v", endpoint, node.NodeID, err) - } - - // Parse the response to extract logs - var logsResp struct { - Result string `json:"result"` - } - if err := json.Unmarshal([]byte(response), &logsResp); err != nil { - return "", fmt.Errorf("failed to decode logs response from node %s: %v", node.NodeID, err) - } - - return logsResp.Result, nil -} - -// mapContainerStatus maps Docker container status to workload status -func mapContainerStatus(dockerStatus string) string { - dockerStatus = strings.ToLower(dockerStatus) - switch { - case strings.Contains(dockerStatus, "pulling"): - return "Pulling" - case strings.Contains(dockerStatus, "imagepullbackoff"): - return "ImagePullBackOff" - case strings.Contains(dockerStatus, "containercreating"): - return "ContainerCreating" - case strings.Contains(dockerStatus, "up"): - return "Running" - case strings.Contains(dockerStatus, "exited"): - return "Exited" - case strings.Contains(dockerStatus, "created"): - return "Scheduled" - case strings.Contains(dockerStatus, "paused"): - return "Stopped" - default: - return "Unknown" - } -} - -// MonitorWorkloads periodically checks container statuses and updates etcd -func (m *Monitor) MonitorWorkloads(ctx context.Context, interval time.Duration) { - ticker := time.NewTicker(interval) - defer ticker.Stop() - - for { - select { - case <-ctx.Done(): - log.Printf("Stopping workload monitoring") - return - case <-ticker.C: - log.Printf("Starting workload monitoring cycle") - nodes, err := m.scheduler.GetNodes() - if err != nil { - log.Printf("Failed to get nodes for monitoring: %v", err) - continue - } - - for _, node := range nodes { - if node.Status != "active" { - log.Printf("Skipping inactive node %s (status: %s)", node.NodeID, node.Status) - continue - } - - // Get workloads for node - workloads, err := m.scheduler.GetWorkloadsByNode(node.NodeID) - if err != nil { - log.Printf("Failed to get workloads for node %s: %v", node.NodeID, err) - continue - } - - // Get container list from agent - containers, err := m.getContainerList(node) - if err != nil { - log.Printf("Failed to get container list for node %s: %v", node.NodeID, err) - continue - } - - // Create map of container names to info - containerMap := make(map[string]ContainerInfo) - for _, container := range containers { - containerMap[container.Name] = container - } - - // Update workload statuses and capture logs - for _, workload := range workloads { - container, exists := containerMap[workload.ID] // Match by workload ID instead of name - newStatus := workload.Status - statusChanged := false - var reason string - if !exists { - if workload.Status != "Missing" { - newStatus = "Missing" - statusChanged = true - log.Printf("Workload %s (name: %s) on node %s not found in container list, setting status to %s", workload.ID, workload.Name, node.NodeID, newStatus) - } - } else { - mappedStatus := mapContainerStatus(container.Status) - if mappedStatus != workload.Status { - newStatus = mappedStatus - statusChanged = true - log.Printf("Workload %s (name: %s) on node %s status changed from %s to %s", workload.ID, workload.Name, node.NodeID, workload.Status, newStatus) - } - // If container has a Reason field, propagate it - if (mappedStatus == "Pulling" || mappedStatus == "ImagePullBackOff" || mappedStatus == "ContainerCreating") && container.Reason != "" { - reason = container.Reason - } - } - if statusChanged { - // Update status - if err := m.scheduler.UpdateWorkloadStatus(workload.ID, newStatus); err != nil { - log.Printf("Failed to update status for workload %s: %v", workload.ID, err) - } - // Update logs with reason if present - if reason != "" { - if err := m.scheduler.UpdateWorkloadLogs(workload.ID, "Reason: "+reason); err != nil { - log.Printf("Failed to update logs for workload %s: %v", workload.ID, err) - } - } - // Capture logs if container exists and status indicates it has run - if exists && (newStatus == "Running" || newStatus == "Exited" || newStatus == "Failed") { - logs, err := m.getContainerLogs(node, container.ID) - if err != nil { - log.Printf("Failed to get logs for container %s (workload %s): %v", container.ID, workload.ID, err) - } else if logs != "" { - if err := m.scheduler.UpdateWorkloadLogs(workload.ID, fmt.Sprintf("Status changed to %s. Container logs:\n%s", newStatus, logs)); err != nil { - log.Printf("Failed to update logs for workload %s: %v", workload.ID, err) - } - } - } - } - } - } - log.Printf("Completed workload monitoring cycle") - } - } -} - -// StartMonitoring initializes the workload monitoring routine -func (m *Monitor) StartMonitoring() context.CancelFunc { - ctx, cancel := context.WithCancel(context.Background()) - go m.MonitorWorkloads(ctx, 60*time.Second) - return cancel -} diff --git a/prow/internal/scheduler/reconciler.go b/prow/internal/scheduler/reconciler.go deleted file mode 100644 index cb31851..0000000 --- a/prow/internal/scheduler/reconciler.go +++ /dev/null @@ -1,452 +0,0 @@ -package scheduler - -import ( - "context" - "encoding/json" - "fmt" - "log" - "time" - - "github.com/persys-dev/prow/internal/models" -) - -const gracePeriod = 2 * time.Minute - -// Reconciler handles reconciliation between desired and actual state -type Reconciler struct { - scheduler *Scheduler - monitor *Monitor -} - -// ReconciliationResult represents the result of a reconciliation operation -type ReconciliationResult struct { - WorkloadID string - DesiredState string - ActualState string - Action string - Success bool - ErrorMessage string - RetryCount int - LastAttempt time.Time -} - -// NewReconciler creates a new Reconciler instance -func NewReconciler(scheduler *Scheduler, monitor *Monitor) *Reconciler { - return &Reconciler{ - scheduler: scheduler, - monitor: monitor, - } -} - -// ReconcileWorkload reconciles a single workload to its desired state -func (r *Reconciler) ReconcileWorkload(workload models.Workload) (*ReconciliationResult, error) { - result := &ReconciliationResult{ - WorkloadID: workload.ID, - DesiredState: workload.DesiredState, - LastAttempt: time.Now(), - } - - // Get current actual state from agent - actualState, err := r.getActualWorkloadState(workload) - if err != nil { - result.ActualState = "Unknown" - result.Action = "Investigate" - result.Success = false - result.ErrorMessage = fmt.Sprintf("Failed to get actual state: %v", err) - return result, err - } - - result.ActualState = actualState - - // Determine if reconciliation is needed - if r.needsReconciliation(workload, actualState) { - action, err := r.performReconciliation(workload, actualState) - result.Action = action - result.Success = err == nil - if err != nil { - result.ErrorMessage = err.Error() - } - } else { - result.Action = "NoAction" - result.Success = true - } - - // Update workload with reconciliation result - r.updateWorkloadReconciliationStatus(workload.ID, result) - - return result, nil -} - -// getActualWorkloadState queries the agent to get the actual state of a workload -func (r *Reconciler) getActualWorkloadState(workload models.Workload) (string, error) { - // Get the node for this workload - node, err := r.scheduler.GetNodeByID(workload.NodeID) - if err != nil { - return "", fmt.Errorf("failed to get node %s: %v", workload.NodeID, err) - } - - // Query agent for container status - containers, err := r.monitor.getContainerList(node) - if err != nil { - return "", fmt.Errorf("failed to get container list: %v", err) - } - - // Find container by workload ID - for _, container := range containers { - if container.Name == workload.ID { - return r.mapContainerStatusToWorkloadState(container.Status), nil - } - } - - // Container not found - return "Missing", nil -} - -// mapContainerStatusToWorkloadState maps Docker container status to workload state -func (r *Reconciler) mapContainerStatusToWorkloadState(dockerStatus string) string { - switch dockerStatus { - case "running": - return "Running" - case "exited": - return "Exited" - case "created": - return "Scheduled" - case "paused": - return "Stopped" - case "restarting": - return "Restarting" - case "Pulling": - return "Pulling" - case "ImagePullBackOff": - return "ImagePullBackOff" - case "ContainerCreating": - return "ContainerCreating" - default: - return "Unknown" - } -} - -// needsReconciliation determines if a workload needs reconciliation -func (r *Reconciler) needsReconciliation(workload models.Workload, actualState string) bool { - desiredState := workload.DesiredState - if desiredState == "" { - desiredState = "Running" - } - - // Grace period for Missing, Pulling, ContainerCreating - if actualState == "Missing" || actualState == "Pulling" || actualState == "ContainerCreating" { - if workload.Metadata != nil { - if lastLaunchRaw, ok := workload.Metadata["lastLaunchTime"]; ok { - var lastLaunch time.Time - switch v := lastLaunchRaw.(type) { - case time.Time: - lastLaunch = v - case string: - t, err := time.Parse(time.RFC3339, v) - if err == nil { - lastLaunch = t - } - } - if !lastLaunch.IsZero() && time.Since(lastLaunch) < gracePeriod { - log.Printf("Workload %s is in %s state, within grace period (%.0fs left), skipping reconciliation", workload.ID, actualState, (gracePeriod - time.Since(lastLaunch)).Seconds()) - return false - } - } - } - } - - // Check if actual state matches desired state - if actualState == desiredState { - return false - } - - // Special cases that don't need reconciliation - if desiredState == "Stopped" && actualState == "Exited" { - return false - } - if desiredState == "Running" && actualState == "Restarting" { - return false // Give it time to restart - } - if actualState == "Pulling" || actualState == "ImagePullBackOff" || actualState == "ContainerCreating" { - log.Printf("Workload %s is in state %s, waiting before reconciling", workload.ID, actualState) - return false // Wait for image pull or container creation - } - - return true -} - -// performReconciliation performs the actual reconciliation action -func (r *Reconciler) performReconciliation(workload models.Workload, actualState string) (string, error) { - desiredState := workload.DesiredState - if desiredState == "" { - desiredState = "Running" - } - - log.Printf("Reconciling workload %s: actual=%s, desired=%s", workload.ID, actualState, desiredState) - - switch { - case actualState == "Missing" && desiredState == "Running": - return r.recreateWorkload(workload) - case actualState == "Exited" && desiredState == "Running": - return r.restartWorkload(workload) - case actualState == "Running" && desiredState == "Stopped": - return r.stopWorkload(workload) - case actualState == "Failed" && desiredState == "Running": - return r.recreateWorkload(workload) - case actualState == "ImagePullBackOff" && desiredState == "Running": - log.Printf("Workload %s is in ImagePullBackOff, will retry image pull on next cycle", workload.ID) - return "WaitForImagePull", nil - default: - return "NoAction", fmt.Errorf("no reconciliation action defined for actual=%s, desired=%s", actualState, desiredState) - } -} - -// recreateWorkload recreates a missing or failed workload -func (r *Reconciler) recreateWorkload(workload models.Workload) (string, error) { - log.Printf("Recreating workload %s", workload.ID) - - // Get the node - node, err := r.scheduler.GetNodeByID(workload.NodeID) - if err != nil { - return "Recreate", fmt.Errorf("failed to get node: %v", err) - } - - // Prepare the recreation payload - var endpoint string - var payload interface{} - - switch workload.Type { - case "docker-container": - endpoint = "/docker/run" - payload = map[string]interface{}{ - "workloadId": workload.ID, - "image": workload.Image, - "name": workload.ID, - "displayName": workload.Name, - "command": workload.Command, - "env": workload.EnvVars, - "ports": workload.Ports, - "volumes": workload.Volumes, - "network": workload.Network, - "restartPolicy": workload.RestartPolicy, - "detach": true, - "async": true, - } - case "docker-compose": - endpoint = "/compose/run" - payload = map[string]interface{}{ - "workloadId": workload.ID, - "displayName": workload.Name, - "composeDir": workload.LocalPath, - "envVariables": workload.EnvVars, - "async": true, - } - default: - return "Recreate", fmt.Errorf("unsupported workload type for recreation: %s", workload.Type) - } - - // Send recreation command - _, err = r.scheduler.SendCommandToNode(node, endpoint, payload) - if err != nil { - return "Recreate", fmt.Errorf("failed to recreate workload: %v", err) - } - - // Update workload status - r.scheduler.UpdateWorkloadStatus(workload.ID, "Recreating") - r.scheduler.UpdateWorkloadLogs(workload.ID, fmt.Sprintf("Workload recreated at %s", time.Now().Format(time.RFC3339))) - - // Update Metadata["lastLaunchTime"] - if workload.Metadata == nil { - workload.Metadata = make(map[string]interface{}) - } - workload.Metadata["lastLaunchTime"] = time.Now() - - return "Recreate", nil -} - -// restartWorkload restarts an exited workload -func (r *Reconciler) restartWorkload(workload models.Workload) (string, error) { - log.Printf("Restarting workload %s", workload.ID) - - node, err := r.scheduler.GetNodeByID(workload.NodeID) - if err != nil { - return "Restart", fmt.Errorf("failed to get node: %v", err) - } - - endpoint := fmt.Sprintf("/docker/start/%s", workload.ID) - _, err = r.scheduler.SendCommandToNode(node, endpoint, nil) - if err != nil { - return "Restart", fmt.Errorf("failed to restart workload: %v", err) - } - - r.scheduler.UpdateWorkloadStatus(workload.ID, "Restarting") - r.scheduler.UpdateWorkloadLogs(workload.ID, fmt.Sprintf("Workload restarted at %s", time.Now().Format(time.RFC3339))) - - // Update Metadata["lastLaunchTime"] - if workload.Metadata == nil { - workload.Metadata = make(map[string]interface{}) - } - workload.Metadata["lastLaunchTime"] = time.Now() - - return "Restart", nil -} - -// stopWorkload stops a running workload -func (r *Reconciler) stopWorkload(workload models.Workload) (string, error) { - log.Printf("Stopping workload %s", workload.ID) - - node, err := r.scheduler.GetNodeByID(workload.NodeID) - if err != nil { - return "Stop", fmt.Errorf("failed to get node: %v", err) - } - - endpoint := fmt.Sprintf("/docker/stop/%s", workload.ID) - _, err = r.scheduler.SendCommandToNode(node, endpoint, nil) - if err != nil { - return "Stop", fmt.Errorf("failed to stop workload: %v", err) - } - - r.scheduler.UpdateWorkloadStatus(workload.ID, "Stopping") - r.scheduler.UpdateWorkloadLogs(workload.ID, fmt.Sprintf("Workload stopped at %s", time.Now().Format(time.RFC3339))) - - // Update Metadata["lastLaunchTime"] - if workload.Metadata == nil { - workload.Metadata = make(map[string]interface{}) - } - workload.Metadata["lastLaunchTime"] = time.Now() - - return "Stop", nil -} - -// updateWorkloadReconciliationStatus updates the workload with reconciliation metadata -func (r *Reconciler) updateWorkloadReconciliationStatus(workloadID string, result *ReconciliationResult) { - workload, err := r.scheduler.GetWorkloadByID(workloadID) - if err != nil { - log.Printf("Failed to get workload %s for reconciliation status update: %v", workloadID, err) - return - } - - // Add reconciliation metadata - if workload.Metadata == nil { - workload.Metadata = make(map[string]interface{}) - } - workload.Metadata["lastReconciliation"] = result.LastAttempt - workload.Metadata["lastReconciliationAction"] = result.Action - workload.Metadata["lastReconciliationSuccess"] = result.Success - workload.Metadata["reconciliationRetryCount"] = result.RetryCount - - // Update the workload - workloadJSON, err := json.Marshal(workload) - if err != nil { - log.Printf("Failed to marshal workload %s: %v", workloadID, err) - return - } - - if err := r.scheduler.RetryableEtcdPut("/workloads/"+workloadID, string(workloadJSON)); err != nil { - log.Printf("Failed to update workload %s reconciliation status: %v", workloadID, err) - } -} - -// ReconcileAllWorkloads reconciles all workloads in the system -func (r *Reconciler) ReconcileAllWorkloads() ([]*ReconciliationResult, error) { - workloads, err := r.scheduler.GetWorkloads() - if err != nil { - return nil, fmt.Errorf("failed to get workloads: %v", err) - } - - var results []*ReconciliationResult - for _, workload := range workloads { - // Skip workloads that are in terminal states - if workload.Status == "Completed" || workload.Status == "Deleted" { - continue - } - - result, err := r.ReconcileWorkload(workload) - if err != nil { - log.Printf("Failed to reconcile workload %s: %v", workload.ID, err) - continue - } - results = append(results, result) - } - - return results, nil -} - -// StartReconciliationLoop starts the continuous reconciliation loop -func (r *Reconciler) StartReconciliationLoop(ctx context.Context, interval time.Duration) { - ticker := time.NewTicker(interval) - defer ticker.Stop() - - log.Printf("Starting reconciliation loop with interval %v", interval) - - for { - select { - case <-ctx.Done(): - log.Printf("Stopping reconciliation loop") - return - case <-ticker.C: - log.Printf("Starting reconciliation cycle") - results, err := r.ReconcileAllWorkloads() - if err != nil { - log.Printf("Reconciliation cycle failed: %v", err) - continue - } - - // Log reconciliation summary - successCount := 0 - actionCount := 0 - for _, result := range results { - if result.Success { - successCount++ - } - if result.Action != "NoAction" { - actionCount++ - } - } - - log.Printf("Reconciliation cycle completed: %d workloads processed, %d actions taken, %d successful", - len(results), actionCount, successCount) - } - } -} - -// GetReconciliationStats returns statistics about reconciliation performance -func (r *Reconciler) GetReconciliationStats() (map[string]interface{}, error) { - workloads, err := r.scheduler.GetWorkloads() - if err != nil { - return nil, err - } - - stats := map[string]interface{}{ - "totalWorkloads": len(workloads), - "needingReconciliation": 0, - "reconciliationErrors": 0, - "lastReconciliation": time.Time{}, - } - - for _, workload := range workloads { - if workload.Metadata != nil { - if lastRecon, ok := workload.Metadata["lastReconciliation"]; ok { - if lastReconTime, ok := lastRecon.(time.Time); ok { - if lastReconTime.After(stats["lastReconciliation"].(time.Time)) { - stats["lastReconciliation"] = lastReconTime - } - } - } - - if success, ok := workload.Metadata["lastReconciliationSuccess"]; ok { - if !success.(bool) { - stats["reconciliationErrors"] = stats["reconciliationErrors"].(int) + 1 - } - } - } - - // Check if workload needs reconciliation - actualState, err := r.getActualWorkloadState(workload) - if err == nil && r.needsReconciliation(workload, actualState) { - stats["needingReconciliation"] = stats["needingReconciliation"].(int) + 1 - } - } - - return stats, nil -} diff --git a/prow/internal/scheduler/scheduler.go b/prow/internal/scheduler/scheduler.go deleted file mode 100644 index 9528a3e..0000000 --- a/prow/internal/scheduler/scheduler.go +++ /dev/null @@ -1,854 +0,0 @@ -package scheduler - -import ( - "bytes" - "context" - "crypto" - "crypto/rand" - "crypto/rsa" - "crypto/sha256" - "crypto/x509" - "encoding/base64" - "encoding/hex" - "encoding/json" - "encoding/pem" - "fmt" - "io" - "io/ioutil" - "log" - "net/http" - "os" - "path/filepath" - "strings" - "time" - - "github.com/google/uuid" - "github.com/persys-dev/prow/internal/models" - clientv3 "go.etcd.io/etcd/client/v3" - gootelhttp "go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp" - "go.opentelemetry.io/otel" -) - -// Constants -const ( - etcdTimeout = 5 * time.Second - maxRetries = 5 - retryWaitTime = 2 * time.Second - keyDir = "~/.prow" - privateKeyFile = "scheduler.key" - publicKeyFile = "scheduler.pub" -) - -// Scheduler holds the state and configuration for the cluster scheduler. -type Scheduler struct { - privateKey *rsa.PrivateKey - sharedSecret string - etcdClient *clientv3.Client - httpClient *http.Client - domain string - publicKeyPEM string - monitor *Monitor - reconciler *Reconciler -} - -// NewScheduler initializes the scheduler with an etcd client and configuration. -func NewScheduler() (*Scheduler, error) { - // Resolve home directory for key storage - homeDir, err := os.UserHomeDir() - if err != nil { - return nil, fmt.Errorf("failed to get home directory: %w", err) - } - keyDirPath := strings.Replace(keyDir, "~", homeDir, 1) - - // Load or generate key pair - privateKey, publicKeyPEM, err := loadOrGenerateKeys(keyDirPath) - if err != nil { - return nil, fmt.Errorf("failed to load/generate keys: %w", err) - } - - // Get etcd endpoints from environment variable, with fallback - etcdEndpoints := os.Getenv("ETCD_ENDPOINTS") - if etcdEndpoints == "" { - etcdEndpoints = "localhost:2379" - } - - // Get domain from environment variable, with fallback - domain := os.Getenv("DOMAIN") - if domain == "" { - domain = "persys.local" - } - - // Get shared secret from environment - sharedSecret := os.Getenv("AGENT_SECRET") - if sharedSecret == "" { - fmt.Print("Warning: AGENT_SECRET not set; TOFU mode will be used unless shared secret is provided") - } - - cli, err := clientv3.New(clientv3.Config{ - Endpoints: strings.Split(etcdEndpoints, ","), - DialTimeout: etcdTimeout, - }) - if err != nil { - return nil, fmt.Errorf("failed to create etcd client: %v", err) - } - - // Verify etcd connection - ctx, cancel := context.WithTimeout(context.Background(), etcdTimeout) - defer cancel() - if _, err := cli.Get(ctx, "/health"); err != nil { - cli.Close() - return nil, fmt.Errorf("failed to connect to etcd: %v", err) - } - - scheduler := &Scheduler{ - etcdClient: cli, - httpClient: &http.Client{ - Transport: gootelhttp.NewTransport(&http.Transport{}), - Timeout: 10 * time.Second, - }, - domain: domain, - privateKey: privateKey, - publicKeyPEM: publicKeyPEM, - sharedSecret: sharedSecret, - } - - // Initialize monitor and reconciler - scheduler.monitor = NewMonitor(scheduler) - scheduler.reconciler = NewReconciler(scheduler, scheduler.monitor) - - return scheduler, nil -} - -// loadOrGenerateKeys loads existing keys or generates new ones -func loadOrGenerateKeys(keyDirPath string) (*rsa.PrivateKey, string, error) { - if err := os.MkdirAll(keyDirPath, 0700); err != nil { - return nil, "", fmt.Errorf("failed to create key directory %s: %w", keyDirPath, err) - } - - privateKeyPath := filepath.Join(keyDirPath, privateKeyFile) - publicKeyPath := filepath.Join(keyDirPath, publicKeyFile) - - // Try to load existing private key - if _, err := os.Stat(privateKeyPath); err == nil { - privateKeyBytes, err := ioutil.ReadFile(privateKeyPath) - if err != nil { - return nil, "", fmt.Errorf("failed to read private key %s: %w", privateKeyPath, err) - } - privateBlock, _ := pem.Decode(privateKeyBytes) - if privateBlock == nil || privateBlock.Type != "RSA PRIVATE KEY" { - return nil, "", fmt.Errorf("invalid private key format in %s", privateKeyPath) - } - privateKey, err := x509.ParsePKCS1PrivateKey(privateBlock.Bytes) - if err != nil { - return nil, "", fmt.Errorf("failed to parse private key: %w", err) - } - - // Load public key - publicKeyBytes, err := ioutil.ReadFile(publicKeyPath) - if err != nil { - return nil, "", fmt.Errorf("failed to read public key %s: %w", publicKeyPath, err) - } - log.Printf("Loaded public key from %s: %s...", publicKeyPath, string(publicKeyBytes)[:50]) - return privateKey, string(publicKeyBytes), nil - } - - // Generate new key pair - privateKey, err := rsa.GenerateKey(rand.Reader, 2048) - if err != nil { - return nil, "", fmt.Errorf("failed to generate private key: %w", err) - } - - // Encode private key - privateKeyBytes := x509.MarshalPKCS1PrivateKey(privateKey) - privateBlock := &pem.Block{ - Type: "RSA PRIVATE KEY", - Bytes: privateKeyBytes, - } - privatePem := new(bytes.Buffer) - if err := pem.Encode(privatePem, privateBlock); err != nil { - return nil, "", fmt.Errorf("failed to encode private key: %w", err) - } - - // Encode public key - publicKeyPEM, err := encodePublicKey(&privateKey.PublicKey) - if err != nil { - return nil, "", fmt.Errorf("failed to encode public key: %w", err) - } - - // Save keys - if err := ioutil.WriteFile(privateKeyPath, privatePem.Bytes(), 0600); err != nil { - return nil, "", fmt.Errorf("failed to write private key %s: %w", privateKeyPath, err) - } - if err := ioutil.WriteFile(publicKeyPath, []byte(publicKeyPEM), 0644); err != nil { - return nil, "", fmt.Errorf("failed to write public key %s: %w", publicKeyPath, err) - } - - log.Printf("Generated and saved new key pair: private=%s, public=%s", privateKeyPath, publicKeyPath) - return privateKey, publicKeyPEM, nil -} - -func encodePublicKey(pubKey *rsa.PublicKey) (string, error) { - pubKeyBytes, err := x509.MarshalPKIXPublicKey(pubKey) - if err != nil { - return "", fmt.Errorf("failed to marshal public key: %w", err) - } - pemBlock := &pem.Block{ - Type: "PUBLIC KEY", - Bytes: pubKeyBytes, - } - pemBuffer := new(bytes.Buffer) - if err := pem.Encode(pemBuffer, pemBlock); err != nil { - return "", fmt.Errorf("failed to encode PEM: %w", err) - } - pemHex := hex.EncodeToString(pemBuffer.Bytes()) - log.Printf("Encoded public key (hex, first 50 chars): %s...", pemHex[:50]) - return pemHex, nil -} - -// Close shuts down the scheduler gracefully. -func (s *Scheduler) Close() error { - if s.etcdClient != nil { - return s.etcdClient.Close() - } - return nil -} - -func (s *Scheduler) RegisterNode(node models.Node) error { - if node.NodeID == "" || node.IPAddress == "" || node.AgentPort == 0 { - return fmt.Errorf("nodeID, IPAddress, and AgentPort are required") - } - if node.TotalCPU <= 0 || node.TotalMemory <= 0 { - return fmt.Errorf("totalCPU and totalMemory must be positive") - } - - node.LastHeartbeat = time.Now() - node.DomainName = node.NodeID + "." + s.domain - if node.Status == "" { - node.Status = "Active" - } - if node.AvailableCPU == 0 { - node.AvailableCPU = node.TotalCPU - } - if node.AvailableMemory == 0 { - node.AvailableMemory = node.TotalMemory - } - if node.Labels == nil { - node.Labels = make(map[string]string) - } - if node.AuthConfig.PublicKey == "" || node.AuthConfig.PublicKey != s.publicKeyPEM { - node.AuthConfig.PublicKey = s.publicKeyPEM - } - if node.AuthConfig.SharedSecret == "" && s.sharedSecret != "" { - node.AuthConfig.SharedSecret = s.sharedSecret - } - - log.Printf("Registering node: %+v", node) - - // Check for stale node data - existingNode, err := s.GetNodeByID(node.NodeID) - if err == nil && (existingNode.AuthConfig.PublicKey != s.publicKeyPEM || existingNode.Status == "HandshakeFailed") { - log.Printf("Node %s has mismatched public key or failed handshake, deleting and re-registering", node.NodeID) - if err := s.DeleteNode(node.NodeID); err != nil { - log.Printf("Failed to delete node %s: %v", node.NodeID, err) - } - } - - // Initiate handshake in a Go routine - go func(node models.Node) { - if err := s.initiateHandshake(node); err != nil { - log.Printf("Failed to initiate handshake with node %s: %v", node.NodeID, err) - // Optionally update node status in etcd - node.Status = "HandshakeFailed" - nodeJSON, _ := json.Marshal(node) - s.RetryableEtcdPut("/nodes/"+node.NodeID, string(nodeJSON)) - } else { - log.Printf("Handshake successful with node %s", node.NodeID) - } - }(node) - - nodeJSON, err := json.Marshal(node) - if err != nil { - return fmt.Errorf("failed to marshal node: %v", err) - } - - if err := s.RetryableEtcdPut("/nodes/"+node.NodeID, string(nodeJSON)); err != nil { - return fmt.Errorf("failed to register node %s: %v", node.NodeID, err) - } - - if err := s.UpdateCoreDNS(node); err != nil { - log.Printf("Failed to update CoreDNS for node %s: %v", node.NodeID, err) - } - - log.Printf("Registered node: %s", node.NodeID) - - return nil -} - -func (s *Scheduler) initiateHandshake(node models.Node) error { - const maxRetries = 3 - const baseDelay = 10 * time.Second - - for attempt := 1; attempt <= maxRetries; attempt++ { - log.Printf("Handshake attempt %d/%d for node %s", attempt, maxRetries, node.NodeID) - - // Prepare handshake payload - payload := map[string]string{ - "schedulerId": "prow-scheduler", - "timestamp": time.Now().UTC().Format(time.RFC3339), - } - payloadBytes, err := json.Marshal(payload) - if err != nil { - log.Printf("Failed to marshal handshake payload for node %s: %v", node.NodeID, err) - continue - } - log.Printf("Handshake payload for node %s: %s", node.NodeID, string(payloadBytes)) - - // Sign payload - hash := sha256.Sum256(payloadBytes) - signature, err := rsa.SignPKCS1v15(rand.Reader, s.privateKey, crypto.SHA256, hash[:]) - if err != nil { - log.Printf("Failed to sign handshake payload for node %s: %v", node.NodeID, err) - continue - } - signatureB64 := base64.StdEncoding.EncodeToString(signature) - log.Printf("Handshake signatureB64 for node %s: %s...", node.NodeID, signatureB64[:50]) - - // Send handshake request - url := fmt.Sprintf("http://%s:%d/api/v1/handshake", node.IPAddress, node.AgentPort) - - time.Sleep(10 * time.Second) - - tr := otel.Tracer("prow-scheduler/handshake") - ctx, span := tr.Start(context.Background(), "initiateHandshake") - defer span.End() - - req, err := http.NewRequestWithContext(ctx, "POST", url, bytes.NewBuffer(payloadBytes)) - if err != nil { - log.Printf("Failed to create handshake request for node %s: %v", node.NodeID, err) - continue - } - req.Header.Set("Content-Type", "application/json") - req.Header.Set("X-Scheduler-Signature", signatureB64) - req.Header.Set("X-Scheduler-PublicKey", s.publicKeyPEM) - log.Printf("Handshake public key for node %s: %s...", node.NodeID, s.publicKeyPEM[:50]) - - resp, err := s.httpClient.Do(req) - if err != nil { - log.Printf("Failed to send handshake request for node %s: %v", node.NodeID, err) - } else if resp.StatusCode != http.StatusOK { - log.Printf("Handshake failed for node %s with status: %d", node.NodeID, resp.StatusCode) - resp.Body.Close() - } else { - resp.Body.Close() - log.Printf("Handshake successful for node %s", node.NodeID) - return nil - } - - // Exponential backoff - if attempt < maxRetries { - delay := baseDelay * time.Duration(1< 10*time.Minute { - log.Printf("Node %s heartbeat too old: %v", node.NodeID, node.LastHeartbeat) - continue - } - if !matchesLabels(workload.Labels, node.Labels) { - log.Printf("Node %s labels %v do not match workload labels %v", node.NodeID, node.Labels, workload.Labels) - continue - } - - selectedNode = node - log.Printf("Selected node %s for workload %s", node.NodeID, workload.ID) - break - } - - if selectedNode.NodeID == "" { - log.Printf("No suitable node found for workload %s", workload.ID) - return "", fmt.Errorf("no suitable node available") - } - - workload.NodeID = selectedNode.NodeID - workload.Status = "Scheduled" - - // Store workload in etcd FIRST before sending command to agent - workloadJSON, err := json.Marshal(workload) - if err != nil { - return "", fmt.Errorf("failed to marshal workload: %v", err) - } - log.Printf("Storing workload %s at /workloads/%s: %s", workload.ID, workload.ID, string(workloadJSON)) - if err := s.RetryableEtcdPut("/workloads/"+workload.ID, string(workloadJSON)); err != nil { - log.Printf("Failed to store workload %s: %v", workload.ID, err) - return "", fmt.Errorf("failed to store workload %s: %v", workload.ID, err) - } - - // Prepare agent API request with workload ID for tracking - var endpoint string - var payload interface{} - switch workload.Type { - case "docker-container": - endpoint = "/docker/run" - payload = map[string]interface{}{ - "workloadId": workload.ID, - "image": workload.Image, - "name": workload.ID, // Use workload ID as container name for consistency - "displayName": workload.Name, // Keep original name for display purposes - "command": workload.Command, - "env": workload.EnvVars, - "ports": workload.Ports, - "volumes": workload.Volumes, - "network": workload.Network, - "restartPolicy": workload.RestartPolicy, - "detach": true, - "async": true, // Tell agent to run asynchronously - } - case "docker-compose": - if workload.LocalPath == "" { - return "", fmt.Errorf("LocalPath required for docker-compose type") - } - endpoint = "/compose/run" - payload = map[string]interface{}{ - "workloadId": workload.ID, - "displayName": workload.Name, - "composeDir": workload.LocalPath, - "envVariables": workload.EnvVars, - "async": true, - } - case "git-compose": - if workload.GitRepo == "" { - return "", fmt.Errorf("GitRepo required for git-compose type") - } - endpoint = "/compose/clone" - payload = map[string]interface{}{ - "workloadId": workload.ID, - "displayName": workload.Name, - "repoUrl": workload.GitRepo, - "branch": workload.GitBranch, - "authToken": workload.GitToken, - "envVariables": workload.EnvVars, - "async": true, - } - default: - return "", fmt.Errorf("unsupported workload type: %s", workload.Type) - } - - // Send command to agent asynchronously - don't wait for completion - go func() { - if response, err := s.SendCommandToNode(selectedNode, endpoint, payload); err != nil { - log.Printf("Failed to send command to node %s for workload %s: %v", selectedNode.NodeID, workload.ID, err) - // Update workload status to failed - s.UpdateWorkloadStatus(workload.ID, "Failed") - s.UpdateWorkloadLogs(workload.ID, fmt.Sprintf("Failed to send command to agent: %v", err)) - } else { - log.Printf("Agent: %s response for workload %s: %v", selectedNode.NodeID, workload.ID, response) - } - }() - - log.Printf("Workload %s successfully scheduled on node %s (async execution)", workload.ID, selectedNode.NodeID) - return selectedNode.NodeID, nil -} - -// GetNodes retrieves all nodes from etcd. -func (s *Scheduler) GetNodes() ([]models.Node, error) { - resp, err := s.RetryableEtcdGet("/nodes/", clientv3.WithPrefix()) - if err != nil { - return nil, fmt.Errorf("failed to get nodes: %v", err) - } - - nodes := make([]models.Node, 0) - if resp == nil { - return nodes, nil - } - - for _, kv := range resp.Kvs { - var node models.Node - if err := json.Unmarshal(kv.Value, &node); err != nil { - log.Printf("Failed to unmarshal node data for key %s: %v", kv.Key, err) - continue - } - nodes = append(nodes, node) - } - - return nodes, nil -} - -// GetNodeByID retrieves a specific node by ID. -func (s *Scheduler) GetNodeByID(nodeID string) (models.Node, error) { - resp, err := s.RetryableEtcdGet("/nodes/" + nodeID) - if err != nil { - return models.Node{}, fmt.Errorf("failed to get node %s: %v", nodeID, err) - } - - if len(resp.Kvs) == 0 { - return models.Node{}, fmt.Errorf("node %s not found", nodeID) - } - - var node models.Node - if err := json.Unmarshal(resp.Kvs[0].Value, &node); err != nil { - return models.Node{}, fmt.Errorf("failed to unmarshal node %s: %v", nodeID, err) - } - - return node, nil -} - -// DeleteNode removes a node from etcd and CoreDNS. -func (s *Scheduler) DeleteNode(nodeID string) error { - ctx, cancel := context.WithTimeout(context.Background(), etcdTimeout) - defer cancel() - _, err := s.etcdClient.Delete(ctx, "/nodes/"+nodeID) - if err != nil { - return fmt.Errorf("failed to delete node %s: %v", nodeID, err) - } - - // Remove from CoreDNS - key := fmt.Sprintf("/skydns/%s/%s", reverseDomain(s.domain), nodeID) - _, err = s.etcdClient.Delete(ctx, key) - if err != nil { - log.Printf("Failed to remove CoreDNS entry for node %s: %v", nodeID, err) - } - - log.Printf("Deleted node: %s", nodeID) - return nil -} - -// GetWorkloads retrieves all workloads from etcd. -func (s *Scheduler) GetWorkloads() ([]models.Workload, error) { - resp, err := s.RetryableEtcdGet("/workloads/", clientv3.WithPrefix()) - if err != nil { - return nil, fmt.Errorf("failed to get workloads: %v", err) - } - - workloads := make([]models.Workload, 0) - if resp == nil { - return workloads, nil - } - - for _, kv := range resp.Kvs { - var workload models.Workload - if err := json.Unmarshal(kv.Value, &workload); err != nil { - log.Printf("Failed to unmarshal workload data for key %s: %v", kv.Key, err) - continue - } - workloads = append(workloads, workload) - } - - return workloads, nil -} - -// GetWorkloadByID retrieves a specific workload by ID. -func (s *Scheduler) GetWorkloadByID(workloadID string) (models.Workload, error) { - resp, err := s.RetryableEtcdGet("/workloads/" + workloadID) - if err != nil { - return models.Workload{}, fmt.Errorf("failed to get workload %s: %v", workloadID, err) - } - - if len(resp.Kvs) == 0 { - return models.Workload{}, fmt.Errorf("workload %s not found", workloadID) - } - - var workload models.Workload - if err := json.Unmarshal(resp.Kvs[0].Value, &workload); err != nil { - return models.Workload{}, fmt.Errorf("failed to unmarshal workload %s: %v", workloadID, err) - } - - return workload, nil -} - -// DeleteWorkload removes a workload from etcd. -func (s *Scheduler) DeleteWorkload(workloadID string) error { - ctx, cancel := context.WithTimeout(context.Background(), etcdTimeout) - defer cancel() - _, err := s.etcdClient.Delete(ctx, "/workloads/"+workloadID) - if err != nil { - return fmt.Errorf("failed to delete workload %s: %v", workloadID, err) - } - log.Printf("Deleted workload: %s", workloadID) - return nil -} - -// UpdateWorkloadStatus updates the status of a workload. -func (s *Scheduler) UpdateWorkloadStatus(workloadID, status string) error { - workload, err := s.GetWorkloadByID(workloadID) - if err != nil { - return err - } - - workload.Status = status - workloadJSON, err := json.Marshal(workload) - if err != nil { - return fmt.Errorf("failed to marshal workload %s: %v", workloadID, err) - } - - if err := s.RetryableEtcdPut("/workloads/"+workloadID, string(workloadJSON)); err != nil { - return fmt.Errorf("failed to update workload %s status: %v", workloadID, err) - } - - log.Printf("Updated workload %s status to %s", workloadID, status) - return nil -} - -// UpdateWorkloadLogs updates the logs of a workload. -func (s *Scheduler) UpdateWorkloadLogs(workloadID, logs string) error { - workload, err := s.GetWorkloadByID(workloadID) - if err != nil { - return err - } - - // Append logs with timestamp - timestamp := time.Now().Format("2006-01-02 15:04:05") - logEntry := fmt.Sprintf("[%s] %s\n", timestamp, logs) - - if workload.Logs == "" { - workload.Logs = logEntry - } else { - workload.Logs += logEntry - } - - workloadJSON, err := json.Marshal(workload) - if err != nil { - return fmt.Errorf("failed to marshal workload %s: %v", workloadID, err) - } - - if err := s.RetryableEtcdPut("/workloads/"+workloadID, string(workloadJSON)); err != nil { - return fmt.Errorf("failed to update workload %s logs: %v", workloadID, err) - } - - log.Printf("Updated workload %s logs", workloadID) - return nil -} - -// GetWorkloadsByNode retrieves all workloads assigned to a specific node. -func (s *Scheduler) GetWorkloadsByNode(nodeID string) ([]models.Workload, error) { - workloads, err := s.GetWorkloads() - if err != nil { - return nil, err - } - - nodeWorkloads := make([]models.Workload, 0) - for _, workload := range workloads { - if workload.NodeID == nodeID { - nodeWorkloads = append(nodeWorkloads, workload) - } - } - - return nodeWorkloads, nil -} - -// MonitorNodes periodically checks node health and updates status. -func (s *Scheduler) MonitorNodes(ctx context.Context) { - ticker := time.NewTicker(1 * time.Minute) - defer ticker.Stop() - for { - select { - case <-ctx.Done(): - log.Println("Stopping node monitoring") - return - case <-ticker.C: - nodes, err := s.GetNodes() - if err != nil { - log.Printf("Error monitoring nodes: %v", err) - continue - } - for _, node := range nodes { - if time.Since(node.LastHeartbeat) > 5*time.Minute { - log.Printf("Node %s inactive, last heartbeat: %v", node.NodeID, node.LastHeartbeat) - node.Status = "Inactive" - nodeJSON, err := json.Marshal(node) - if err != nil { - log.Printf("Failed to marshal node %s: %v", node.NodeID, err) - continue - } - if err := s.RetryableEtcdPut("/nodes/"+node.NodeID, string(nodeJSON)); err != nil { - log.Printf("Failed to update node %s status: %v", node.NodeID, err) - } else { - log.Printf("Updated node %s to Inactive", node.NodeID) - } - } - } - } - } -} - -// StartMonitoring starts both node monitoring and workload monitoring -func (s *Scheduler) StartMonitoring(ctx context.Context) { - // Start node monitoring - go s.MonitorNodes(ctx) - - // Start workload monitoring - if s.monitor != nil { - go s.monitor.MonitorWorkloads(ctx, 60*time.Second) - } -} - -// StartReconciliation starts the reconciliation loop -func (s *Scheduler) StartReconciliation(ctx context.Context) { - if s.reconciler != nil { - go s.reconciler.StartReconciliationLoop(ctx, 2*time.Minute) - } -} - -// GetReconciliationStats returns reconciliation statistics -func (s *Scheduler) GetReconciliationStats() (map[string]interface{}, error) { - if s.reconciler != nil { - return s.reconciler.GetReconciliationStats() - } - return nil, fmt.Errorf("reconciler not initialized") -} - -// ReconcileAllWorkloads performs reconciliation on all workloads -func (s *Scheduler) ReconcileAllWorkloads() ([]*ReconciliationResult, error) { - if s.reconciler != nil { - return s.reconciler.ReconcileAllWorkloads() - } - return nil, fmt.Errorf("reconciler not initialized") -} - -// ReconcileWorkload performs reconciliation on a specific workload -func (s *Scheduler) ReconcileWorkload(workload models.Workload) (*ReconciliationResult, error) { - if s.reconciler != nil { - return s.reconciler.ReconcileWorkload(workload) - } - return nil, fmt.Errorf("reconciler not initialized") -} diff --git a/prow/sample.env b/prow/sample.env deleted file mode 100644 index 9a35e7f..0000000 --- a/prow/sample.env +++ /dev/null @@ -1,23 +0,0 @@ -# Persys Prow Scheduler Sample Environment Variables - -# ETCD endpoints (comma-separated) -ETCD_ENDPOINTS=etcd:2379 - -# Domain for service discovery -DOMAIN=cluster.local.dev - -# Shared secret for agent authentication (HMAC) -AGENT_SECRET=your_shared_secret - -# CoreDNS registration -SCHEDULER_IP= # Optionally set the scheduler's IP address for CoreDNS registration -PORT=8084 # Port for the scheduler (default 8084) - -# Certificate management -CA_FILE=/etc/prow/certs/ca.pem -CFSSL_API_URL=https://persys-cfssl:8888 -CERT_COMMON_NAME=prow-scheduler -CERT_ORGANIZATION=persys - -# Logging/Tracing -JAEGER_ENDPOINT=jaeger:4318 \ No newline at end of file diff --git a/tests/e2e/Dockerfile.test-client b/tests/e2e/Dockerfile.test-client index d528007..7aeb9bf 100644 --- a/tests/e2e/Dockerfile.test-client +++ b/tests/e2e/Dockerfile.test-client @@ -1,29 +1,16 @@ FROM golang:1.24-alpine AS builder +WORKDIR /src +COPY persys-scheduler ./persys-scheduler +RUN cd /src/persys-scheduler && go build -o /out/smoke-client ./cmd/smoke-client +COPY tests/e2e ./tests/e2e +RUN cd /src/tests/e2e && go build -o /out/test-runner ./test-runner.go + +FROM alpine:3.21 +RUN apk add --no-cache ca-certificates WORKDIR /app +COPY --from=builder /out/smoke-client ./smoke-client +COPY --from=builder /out/test-runner ./test-runner +RUN chmod +x ./smoke-client ./test-runner -# Copy go mod files -COPY go.mod ./ -RUN go mod download - -# Copy source code -COPY test-runner.go ./ - -# Build the test runner -RUN CGO_ENABLED=0 GOOS=linux go build -a -installsuffix cgo -o test-runner . - -# Final stage -FROM alpine:latest - -RUN apk --no-cache add ca-certificates - -WORKDIR /root/ - -# Copy the binary from builder stage -COPY --from=builder /app/test-runner . - -# Make it executable -RUN chmod +x test-runner - -# Run the test runner -CMD ["./test-runner"] \ No newline at end of file +CMD ["./test-runner"] diff --git a/tests/e2e/Makefile b/tests/e2e/Makefile index a774699..ae6892e 100644 --- a/tests/e2e/Makefile +++ b/tests/e2e/Makefile @@ -1,48 +1,21 @@ -.PHONY: help test test-docker test-script test-go clean +.PHONY: help test test-docker test-go clean -# Default target help: - @echo "Persys Cloud E2E Testing" - @echo "========================" - @echo "" - @echo "Available targets:" - @echo " test - Run all E2E tests (default: script-based)" - @echo " test-docker - Run E2E tests using Docker Compose" - @echo " test-script - Run E2E tests using bash script" - @echo " test-go - Run E2E tests using Go test runner" - @echo " clean - Clean up test environment" - @echo "" + @echo "Persys Scheduler E2E" + @echo "====================" + @echo "test - Run docker-compose based scheduler + compute-agent lifecycle suite" + @echo "test-docker - Same as test" + @echo "test-go - Run Go lifecycle suite against locally-running scheduler/agent" + @echo "clean - Tear down compose test stack" -# Default test target -test: test-script +test: test-docker -# Run tests using Docker Compose test-docker: - @echo "🐳 Running E2E tests with Docker Compose..." docker compose -f docker-compose.test.yml up --build --abort-on-container-exit --exit-code-from test-client docker compose -f docker-compose.test.yml down -# Run tests using bash script -test-script: - @echo "📜 Running E2E tests with bash script..." - @chmod +x test-suite.sh - ./test-suite.sh - -# Run tests using Go test runner test-go: - @echo "🐹 Running E2E tests with Go test runner..." - @if [ ! -f test-runner ]; then \ - echo "Building test runner..."; \ - go build -o test-runner test-runner.go; \ - fi - ./test-runner + go run test-runner.go -# Clean up test environment clean: - @echo "🧹 Cleaning up test environment..." docker compose -f docker-compose.test.yml down -v 2>/dev/null || true - rm -rf test-configs - rm -f test-runner - docker ps -a --filter "name=persys-cloud-test" --format "{{.ID}}" | xargs -r docker rm -f - docker images --filter "label=persys-cloud-test" --format "{{.ID}}" | xargs -r docker rmi -f - @echo "✅ Cleanup complete!" \ No newline at end of file diff --git a/tests/e2e/README.md b/tests/e2e/README.md index 9cdc225..ed92296 100644 --- a/tests/e2e/README.md +++ b/tests/e2e/README.md @@ -1,396 +1,58 @@ -# Persys Cloud End-to-End Testing +# Persys Scheduler E2E Tests -This directory contains comprehensive end-to-end tests for the persys-cloud system, designed to validate the complete workflow from client request through workload execution, monitoring, and reconciliation. +This suite validates scheduler behavior against the real `compute-agent` submodule. -## 🎯 Overview +## Scope -The E2E tests cover: +The tests cover: -- **Full System Integration**: All components working together -- **Authentication Flow**: mTLS, HMAC, and OAuth authentication -- **Service Discovery**: CoreDNS-based scheduler discovery -- **Workload Lifecycle**: Complete workflow from creation to execution -- **Reconciliation**: Self-healing capabilities -- **Error Handling**: Failure scenarios and recovery -- **Performance**: System performance under load +- Scheduler startup (`/health`, `/metrics`) +- Compute-agent startup (`/health`) +- Scheduler gRPC control API lifecycle through Go runner + `cmd/smoke-client` + - retries apply until scheduler can place workload + - `ApplyWorkload` (container) + - `GetWorkload` + - `ListWorkloads` + - `GetClusterSummary` + - `DeleteWorkload` +- Scheduler Prometheus metrics exposure for gRPC, outbound agent RPC, reconciliation, node and workload state gauges -## 🏗️ Test Architecture +## Quick start -``` -┌─────────────────┐ ┌─────────────────┐ ┌─────────────────┐ -│ Test Client │ │ API Gateway │ │ Prow Scheduler │ -│ (persys-cli) │───▶│ (mTLS) │───▶│ (CoreDNS) │ -└─────────────────┘ └─────────────────┘ └─────────────────┘ - │ │ - ▼ ▼ - ┌─────────────────┐ ┌─────────────────┐ - │ CoreDNS │ │ Persys Agent │ - │ (Discovery) │ │ (HMAC) │ - └─────────────────┘ └─────────────────┘ - │ - ▼ - ┌─────────────────┐ - │ Docker │ - │ (Containers) │ - └─────────────────┘ -``` - -## 🚀 Quick Start - -### Prerequisites - -- Docker and Docker Compose -- Go 1.21+ (for Go-based tests) -- curl and jq (for script-based tests) -- OpenSSL (for certificate generation) - -### 1. Set up the test environment - -```bash -make setup -``` - -This creates the necessary configuration files and directories. - -### 2. Generate test certificates (optional) - -```bash -make certs -``` - -This generates mTLS certificates for testing authentication. - -### 3. Run the tests - -Choose one of the following approaches: - -#### Option A: Script-based tests (Recommended for quick testing) - -```bash -make test-script -``` - -#### Option B: Docker Compose tests (Recommended for CI/CD) +### Docker-based (recommended) ```bash +cd tests/e2e make test-docker ``` -#### Option C: Go-based tests (Recommended for development) - -```bash -make test-go -``` - -## 📋 Test Scenarios - -### 1. Basic Workflow Test -- ✅ Service health checks -- ✅ Workload creation via API Gateway -- ✅ Workload execution on Persys Agent -- ✅ Container status verification -- ✅ Metrics endpoint accessibility - -### 2. Authentication Flow Test -- 🔐 mTLS authentication between components -- 🔐 HMAC authentication between Prow and Agent -- 🔐 Invalid authentication rejection - -### 3. Service Discovery Test -- 🔍 CoreDNS-based scheduler discovery -- 🔍 Discovery fallback mechanisms -- 🔍 Multiple scheduler scenarios +This starts: -### 4. Async Execution Test -- ⚡ Immediate response validation -- ⚡ Background execution verification -- ⚡ No HTTP timeout confirmation +- `etcd` +- `compute-agent` (real runtime node service from `compute-agent/`) +- `persys-scheduler` (with `-insecure`) +- `test-client` (runs `test-runner`) -### 5. Reconciliation Test -- 🔄 Container restart verification -- 🔄 Reconciliation statistics -- 🔄 Self-healing capabilities - -### 6. Error Handling Test -- ⚠️ Invalid workload rejection -- ⚠️ Network failure handling -- ⚠️ System stability verification - -## 🛠️ Implementation Approaches - -### Approach 1: Docker Compose Integration Tests - -**Best for**: CI/CD pipelines, consistent environments - -```bash -# Run with Docker Compose -make test-docker -``` - -**Features**: -- Complete containerized environment -- Automatic service discovery -- Built-in monitoring (Prometheus/Grafana) -- Easy cleanup and isolation - -### Approach 2: Script-based Tests - -**Best for**: Quick testing, debugging, manual validation - -```bash -# Run with bash script -make test-script -``` - -**Features**: -- Simple and flexible -- Easy to debug -- Language agnostic -- Minimal dependencies - -### Approach 3: Go-based Tests - -**Best for**: Development, type safety, comprehensive testing +### Go runner only (against existing local scheduler) ```bash -# Run with Go test runner -make test-go -``` - -**Features**: -- Type-safe testing -- Good integration with existing codebase -- Comprehensive error handling -- Structured test reporting - -## 📊 Test Results and Reporting - -### Test Output Example - +cd tests/e2e +SCHEDULER_METRICS_URL=http://localhost:8084 \ +SCHEDULER_GRPC_ADDR=localhost:8085 \ +AGENT_METRICS_URL=http://localhost:8080 \ +go run test-runner.go ``` -🚀 Starting Persys Cloud E2E Tests... -====================================== - -⏳ Waiting for services to be ready... -✅ API Gateway is ready -✅ Prow Scheduler is ready -✅ Persys Agent is ready - -📋 Running test scenarios... -============================ -📋 Running test: Service Health -✅ Service Health passed (2s) -📋 Running test: Workload Creation -✅ Workload Creation passed (3s) -📋 Running test: Workload Execution -✅ Workload Execution passed (12s) -📋 Running test: Metrics Endpoint -✅ Metrics Endpoint passed (1s) -📋 Running test: Workload Listing -✅ Workload Listing passed (1s) -📋 Running test: Reconciliation Stats -✅ Reconciliation Stats passed (1s) -📋 Running test: Invalid Workload -✅ Invalid Workload passed (1s) -📋 Running test: Async Execution -✅ Async Execution passed (2s) - -📊 Test Results -=============== -Total tests: 8 -Passed: 8 -Failed: 0 - -✅ All tests passed! 🎉 -``` - -### Test Metrics - -The tests collect and report: - -- **Response Time**: API response times -- **Throughput**: Workloads per second -- **Success Rate**: Percentage of successful operations -- **Error Rate**: Percentage of failed operations -- **Resource Usage**: CPU, memory, disk usage -- **Reconciliation Time**: Time to detect and fix issues - -## 🔧 Configuration - -### Environment Variables - -```bash -# Service URLs -API_GATEWAY_URL=https://localhost:8551 -PROW_SCHEDULER_URL=https://localhost:8085 -PERSYS_AGENT_URL=http://localhost:8080 - -# Test Configuration -TEST_TIMEOUT=30 -RETRY_INTERVAL=2 -``` - -### Test Configuration Files - -- `docker-compose.test.yml`: Docker Compose test environment -- `test-configs/`: Configuration files for all services -- `test-suite.sh`: Bash script test runner -- `test-runner.go`: Go-based test runner - -## 🧪 Advanced Testing - -### Performance Testing - -```bash -make test-performance -``` - -Tests system performance under various loads. - -### Load Testing - -```bash -make test-load -``` - -Tests system behavior under high load conditions. - -### Security Testing - -```bash -make test-security -``` - -Tests security aspects including authentication and authorization. - -### Chaos Engineering - -```bash -# Stop a service and verify recovery -docker-compose -f docker-compose.test.yml stop api-gateway -# Wait and verify system recovery -make test-basic -``` - -## 🐛 Troubleshooting - -### Common Issues - -1. **Services not starting** - ```bash - # Check service logs - docker-compose -f docker-compose.test.yml logs - - # Restart services - docker-compose -f docker-compose.test.yml restart - ``` - -2. **Certificate issues** - ```bash - # Regenerate certificates - make certs - ``` - -3. **Network connectivity** - ```bash - # Check network connectivity - docker network ls - docker network inspect persys-cloud-test - ``` - -4. **Test failures** - ```bash - # Run with verbose output - ./test-suite.sh --verbose - - # Check individual service health - curl -f http://localhost:8080/health - ``` - -### Debug Mode - -```bash -# Enable debug output -DEBUG=1 make test-script - -# Run with detailed logging -docker-compose -f docker-compose.test.yml up --build --abort-on-container-exit --exit-code-from test-client --verbose -``` - -## 📈 Continuous Integration - -### GitHub Actions Example - -```yaml -name: E2E Tests -on: [push, pull_request] - -jobs: - e2e-tests: - runs-on: ubuntu-latest - steps: - - uses: actions/checkout@v3 - - - name: Set up Docker - uses: docker/setup-buildx-action@v2 - - - name: Run E2E Tests - run: | - cd tests/e2e - make setup - make test-docker - - - name: Upload test results - uses: actions/upload-artifact@v3 - with: - name: test-results - path: tests/e2e/test-results/ -``` - -## 🔄 Maintenance - -### Regular Tasks - -1. **Update test dependencies** - ```bash - go mod tidy - docker-compose -f docker-compose.test.yml pull - ``` - -2. **Clean up test artifacts** - ```bash - make clean - ``` - -3. **Update test scenarios** - - Add new test cases to `test-suite.sh` - - Update Go test runner with new scenarios - - Modify Docker Compose configuration as needed - -### Test Data Management - -- Test workloads are defined in the test scripts -- Test certificates are generated automatically -- Test configurations are version controlled -- Test results are stored in `test-results/` directory - -## 📚 Additional Resources - -- [E2E Testing Strategy](../docs/e2e-testing-strategy.md): Comprehensive testing strategy -- [System Architecture](../docs/architecture.md): System architecture overview -- [API Documentation](../docs/api.md): API reference -- [Troubleshooting Guide](../docs/troubleshooting.md): Common issues and solutions - -## 🤝 Contributing -When adding new tests: +## Environment variables -1. Follow the existing test structure -2. Add appropriate error handling -3. Include meaningful test descriptions -4. Update this README if needed -5. Ensure tests are idempotent +- `SCHEDULER_METRICS_URL` (default `http://localhost:8084`) +- `SCHEDULER_GRPC_ADDR` (default `localhost:8085`) +- `AGENT_METRICS_URL` (default `http://compute-agent:8080`) +- `TEST_WORKLOAD_ID` (default `e2e-workload-1`) +- `RETRY_INTERVAL` (default `2`) +- `MAX_RETRIES` (default `40`) -## 📄 License +## Notes -This testing framework is part of the persys-cloud project and follows the same license terms. \ No newline at end of file +- Docker-based tests run `compute-agent` with `PERSYS_TLS_ENABLED=false` for plaintext gRPC compatibility with the scheduler E2E mode. +- The compose file mounts `/var/run/docker.sock` into `compute-agent` so container workloads can be applied during tests. diff --git a/tests/e2e/docker-compose.test.yml b/tests/e2e/docker-compose.test.yml index ef11b9e..64511a5 100644 --- a/tests/e2e/docker-compose.test.yml +++ b/tests/e2e/docker-compose.test.yml @@ -1,135 +1,66 @@ services: - # --- etcd (Key-value store for Prow and CoreDNS) --- etcd: image: quay.io/coreos/etcd:v3.5.0 - command: ["etcd", "--advertise-client-urls=http://0.0.0.0:2379", "--listen-client-urls=http://0.0.0.0:2379"] + command: ["etcd", "--advertise-client-urls=http://etcd:2379", "--listen-client-urls=http://0.0.0.0:2379"] ports: - "2379:2379" networks: - persys-cloud-test - # --- CoreDNS (Service discovery) --- - coredns: - image: coredns/coredns:1.11.1 - command: ["-conf", "/etc/coredns/Corefile"] - ports: - - "53:53/udp" - networks: - - persys-cloud-test - volumes: - - ./coredns:/etc/coredns - depends_on: - - etcd - - # --- Certificate Authority (CFSSL) --- - persys-cfssl: - build: ../../persys-cfssl - volumes: - - certs:/app/certs - ports: - - 8888:8888 - networks: - - persys-cloud-test - - # --- MongoDB (Database for API Gateway) --- - mongodb: - image: mongo:latest - environment: - MONGO_INITDB_ROOT_USERNAME: admin - MONGO_INITDB_ROOT_PASSWORD: admin - MONGODB_DATABASE: api-gateway - ports: - - 27017:27017 - networks: - - persys-cloud-test - volumes: - - mongodb_data_container:/data/db - - # --- API Gateway --- - api-gateway: + compute-agent: build: - context: ../../api-gateway + context: ../../compute-agent dockerfile: Dockerfile - ports: - - "8551:8551" - # - "8085:8085" environment: - - COREDNS_ADDR=coredns:53 - - ETCD_ENDPOINTS=etcd:2379 - - MONGODB_URI=mongodb://admin:admin@mongodb:27017/api-gateway?authSource=admin - - CFSSL_API_URL=https://persys-cfssl:8888 - - CA_FILE=/etc/api-gateway/certs/ca.pem + - PERSYS_TLS_ENABLED=false + - PERSYS_GRPC_PORT=50051 + - PERSYS_LOG_LEVEL=info + - PERSYS_RECONCILE_ENABLED=false + - PERSYS_VM_ENABLED=false + - PERSYS_SCHEDULER_INSECURE=true + - PERSYS_SCHEDULER_ADDR=persys-scheduler:8085 + user: "0:0" volumes: - - certs:/etc/api-gateway/certs + - /var/run/docker.sock:/var/run/docker.sock + ports: + - "50051:50051" + - "8080:8080" networks: - persys-cloud-test - depends_on: - - coredns - - etcd - - mongodb - - persys-cfssl - # --- Prow Scheduler --- - persys-prow: + persys-scheduler: build: - context: ../../prow + context: ../../persys-scheduler dockerfile: Dockerfile + command: ["./persys-scheduler", "-insecure"] + environment: + - ETCD_ENDPOINTS=etcd:2379 + - GRPC_PORT=8085 + - METRICS_PORT=8084 ports: - "8084:8084" - "8085:8085" - networks: - - persys-cloud-test - environment: - - ETCD_ENDPOINTS=etcd:2379 - - DOMAIN=persys.local - - CFSSL_API_URL=https://persys-cfssl:8888 - - CA_FILE=/etc/prow/certs/ca.pem - volumes: - - certs:/etc/prow/certs depends_on: - etcd - - persys-cfssl - - # --- Persys Agent --- - persys-agent: - build: - context: ../../persys-agent - dockerfile: Dockerfile.ubuntu - ports: - - "8080:8080" + - compute-agent networks: - persys-cloud-test - volumes: - - /var/run/docker.sock:/var/run/docker.sock - - certs:/etc/persys-agent/certs - environment: - - CENTRAL_URL=http://persys-prow:8084 - depends_on: - - persys-prow - # --- Test Client --- test-client: build: - context: . - dockerfile: Dockerfile.test-client + context: ../../ + dockerfile: tests/e2e/Dockerfile.test-client environment: - - API_GATEWAY_URL=http://api-gateway:8085 - - PROW_SCHEDULER_URL=http://persys-prow:8084 - - PERSYS_AGENT_URL=http://persys-agent:8080 + - SCHEDULER_METRICS_URL=http://persys-scheduler:8084 + - SCHEDULER_GRPC_ADDR=persys-scheduler:8085 + - AGENT_METRICS_URL=http://compute-agent:8080 + - TEST_WORKLOAD_ID=e2e-workload-1 + depends_on: + - persys-scheduler + - compute-agent networks: - persys-cloud-test - volumes: - - certs:/etc/test-client/certs - depends_on: - - api-gateway - - persys-prow - - persys-agent command: ["./test-runner"] - -volumes: - certs: - mongodb_data_container: - networks: - persys-cloud-test: \ No newline at end of file + persys-cloud-test: diff --git a/tests/e2e/test-runner.go b/tests/e2e/test-runner.go index e44b820..4d3e2de 100644 --- a/tests/e2e/test-runner.go +++ b/tests/e2e/test-runner.go @@ -2,480 +2,330 @@ package main import ( "bytes" + "context" "encoding/json" "fmt" + "io" "log" "net/http" "os" + "os/exec" + "strconv" + "strings" "time" ) -// HTTPClient extends http.Client with JSON methods -type HTTPClient struct { - *http.Client -} +func main() { + cfg := loadConfig() + client := &http.Client{Timeout: 10 * time.Second} -// TestSuite represents the E2E test suite -type TestSuite struct { - apiGatewayURL string - prowSchedulerURL string - persysAgentURL string - httpClient *HTTPClient -} + must(waitForHTTP(cfg.schedulerMetricsURL+"/health", client, cfg.maxRetries, cfg.retryInterval)) + must(waitForHTTP(cfg.schedulerMetricsURL+"/metrics", client, cfg.maxRetries, cfg.retryInterval)) + must(waitForHTTP(cfg.agentMetricsURL+"/health", client, cfg.maxRetries, cfg.retryInterval)) + must(waitForRegisteredNode(cfg)) -// TestResult represents a test result -type TestResult struct { - Name string `json:"name"` - Status string `json:"status"` - Duration string `json:"duration"` - Error string `json:"error,omitempty"` - Timestamp time.Time `json:"timestamp"` -} + logStep("apply workload") + must(applyWorkloadWithRetry(cfg)) -// TestReport represents the overall test report -type TestReport struct { - TestRunID string `json:"testRunId"` - Timestamp time.Time `json:"timestamp"` - Duration string `json:"duration"` - Results []TestResult `json:"results"` - TotalTests int `json:"totalTests"` - PassedTests int `json:"passedTests"` - FailedTests int `json:"failedTests"` - Status string `json:"status"` -} - -func main() { - // Get environment variables - apiGatewayURL := os.Getenv("API_GATEWAY_URL") - prowSchedulerURL := os.Getenv("PROW_SCHEDULER_URL") - persysAgentURL := os.Getenv("PERSYS_AGENT_URL") + var workload getWorkloadResponse + must(poll(cfg.maxRetries, cfg.retryInterval, func() (bool, error) { + out, err := runSmokeCapture(cfg.schedulerGRPCAddr, "-op", "get-workload", "-workload-id", cfg.testWorkloadID) + if err != nil { + return false, nil + } + if err := unmarshalSmokeJSON(out, &workload); err != nil { + return false, nil + } + s := strings.ToLower(strings.TrimSpace(workload.Workload.Status)) + return s == "running" || s == "pending" || s == "unknown", nil + })) - if apiGatewayURL == "" || prowSchedulerURL == "" || persysAgentURL == "" { - log.Fatal("Missing required environment variables") - } + listOut, err := runSmokeCapture(cfg.schedulerGRPCAddr, "-op", "list-workloads") + must(err) + _ = listOut + must(waitForWorkloadInList(cfg)) - // Create test suite - testSuite := &TestSuite{ - apiGatewayURL: apiGatewayURL, - prowSchedulerURL: prowSchedulerURL, - persysAgentURL: persysAgentURL, - httpClient: &HTTPClient{ - Client: &http.Client{ - Timeout: 30 * time.Second, - }, - }, + summaryOut, err := runSmokeCapture(cfg.schedulerGRPCAddr, "-op", "cluster-summary") + if err != nil { + failf("cluster-summary failed: %v", err) } - - // Run tests - testSuite.RunAllTests() -} - -// RunAllTests runs all E2E tests -func (suite *TestSuite) RunAllTests() { - fmt.Println("🚀 Starting Persys Cloud E2E Tests...") - - // Wait for services to be ready - suite.waitForServices() - - // Run test scenarios - tests := []struct { - name string - fn func() error - }{ - {"Basic Workflow", suite.testBasicWorkflow}, - {"Authentication Flow", suite.testAuthenticationFlow}, - {"Service Discovery", suite.testServiceDiscovery}, - {"Async Execution", suite.testAsyncExecution}, - {"Reconciliation", suite.testReconciliation}, - {"Error Handling", suite.testErrorHandling}, + var summary clusterSummaryResponse + must(unmarshalSmokeJSON(summaryOut, &summary)) + if summary.TotalWorkloads < 1 { + failf("cluster summary reported total_workloads=%d, want >=1", summary.TotalWorkloads) } - passed := 0 - failed := 0 - - for _, test := range tests { - fmt.Printf("📋 Running %s Test...\n", test.name) - start := time.Now() - - err := test.fn() - duration := time.Since(start) - + logStep("delete workload") + must(runSmoke(cfg.schedulerGRPCAddr, "-op", "delete-workload", "-workload-id", cfg.testWorkloadID)) + must(poll(cfg.maxRetries, cfg.retryInterval, func() (bool, error) { + out, err := runSmokeCapture(cfg.schedulerGRPCAddr, "-op", "get-workload", "-workload-id", cfg.testWorkloadID) if err != nil { - fmt.Printf("❌ %s failed: %v (duration: %v)\n", test.name, err, duration) - failed++ - } else { - fmt.Printf("✅ %s passed (duration: %v)\n", test.name, duration) - passed++ + return true, nil // NotFound is acceptable terminal state. + } + var r getWorkloadResponse + if err := unmarshalSmokeJSON(out, &r); err != nil { + return false, nil + } + s := strings.ToLower(strings.TrimSpace(r.Workload.Status)) + return s == "deleting" || s == "deleted", nil + })) + + body, err := getBody(cfg.schedulerMetricsURL+"/metrics", client) + must(err) + required := []string{ + "persys_scheduler_grpc_server_requests_total", + "persys_scheduler_grpc_server_request_duration_seconds", + "persys_scheduler_agent_rpc_requests_total", + "persys_scheduler_agent_rpc_duration_seconds", + "persys_scheduler_nodes_status", + "persys_scheduler_workloads_status", + "persys_scheduler_reconciliation_results_total", + } + for _, metric := range required { + if !strings.Contains(body, metric) { + failf("expected metric %q not found", metric) } } - fmt.Printf("\n📊 Test Results: %d passed, %d failed\n", passed, failed) - fmt.Println("✅ E2E Tests completed!") + fmt.Println("E2E scheduler + compute-agent suite passed") } -// Helper methods - -func (suite *TestSuite) waitForServices() { - fmt.Println("⏳ Waiting for services to be ready...") - - services := []struct { - name string - url string - }{ - {"API Gateway", suite.apiGatewayURL + "/metrics"}, - {"Prow Scheduler", suite.prowSchedulerURL + "/metrics"}, - {"Persys Agent", suite.persysAgentURL + "/metrics"}, - } - - for _, service := range services { - suite.waitForService(service.name, service.url) - } +type config struct { + schedulerMetricsURL string + schedulerGRPCAddr string + agentMetricsURL string + testWorkloadID string + retryInterval time.Duration + maxRetries int } -func (suite *TestSuite) waitForService(name, url string) { - maxRetries := 30 - for i := 0; i < maxRetries; i++ { - resp, err := suite.httpClient.Get(url) - if err == nil && resp.StatusCode == 200 { - fmt.Printf("✅ %s is ready\n", name) - return - } - time.Sleep(2 * time.Second) - } - log.Fatalf("❌ %s failed to start", name) +type getWorkloadResponse struct { + Workload workloadView `json:"workload"` } -func (suite *TestSuite) testBasicWorkflow() error { - // Test 1: Verify services are accessible - if err := suite.testServiceAccessibility(); err != nil { - return fmt.Errorf("service accessibility: %w", err) - } - - // Test 2: Create and execute a simple workload - if err := suite.testWorkloadCreation(); err != nil { - return fmt.Errorf("workload creation: %w", err) - } - - // Test 3: Verify workload execution - if err := suite.testWorkloadExecution(); err != nil { - return fmt.Errorf("workload execution: %w", err) - } - - // Test 4: Verify monitoring - if err := suite.testMonitoring(); err != nil { - return fmt.Errorf("monitoring: %w", err) - } - - return nil +type listWorkloadsResponse struct { + Workloads []workloadView `json:"workloads"` } -func (suite *TestSuite) testAuthenticationFlow() error { - // Test mTLS authentication - if err := suite.testMTLSAuthentication(); err != nil { - return fmt.Errorf("mTLS authentication: %w", err) - } - - // Test HMAC authentication - if err := suite.testHMACAuthentication(); err != nil { - return fmt.Errorf("HMAC authentication: %w", err) - } - - // Test invalid authentication - if err := suite.testInvalidAuthentication(); err != nil { - return fmt.Errorf("invalid authentication: %w", err) - } - - return nil +type workloadView struct { + WorkloadID string `json:"workload_id"` + Status string `json:"status"` } -func (suite *TestSuite) testServiceDiscovery() error { - // Test scheduler discovery - if err := suite.testSchedulerDiscovery(); err != nil { - return fmt.Errorf("scheduler discovery: %w", err) - } - - // Test discovery fallback - if err := suite.testDiscoveryFallback(); err != nil { - return fmt.Errorf("discovery fallback: %w", err) - } - - return nil +type clusterSummaryResponse struct { + TotalWorkloads int32 `json:"total_workloads"` } -func (suite *TestSuite) testAsyncExecution() error { - // Test immediate response - if err := suite.testImmediateResponse(); err != nil { - return fmt.Errorf("immediate response: %w", err) +func loadConfig() config { + return config{ + schedulerMetricsURL: getenvDefault("SCHEDULER_METRICS_URL", "http://localhost:8084"), + schedulerGRPCAddr: getenvDefault("SCHEDULER_GRPC_ADDR", "localhost:8085"), + agentMetricsURL: getenvDefault("AGENT_METRICS_URL", "http://compute-agent:8080"), + testWorkloadID: getenvDefault("TEST_WORKLOAD_ID", "e2e-workload-1"), + retryInterval: getenvDurationDefault("RETRY_INTERVAL", 2*time.Second), + maxRetries: getenvIntDefault("MAX_RETRIES", 40), } - - // Test background execution - if err := suite.testBackgroundExecution(); err != nil { - return fmt.Errorf("background execution: %w", err) - } - - return nil } -func (suite *TestSuite) testReconciliation() error { - // Test container restart - if err := suite.testContainerRestart(); err != nil { - return fmt.Errorf("container restart: %w", err) - } - - // Test reconciliation statistics - if err := suite.testReconciliationStats(); err != nil { - return fmt.Errorf("reconciliation stats: %w", err) - } - - return nil +func applyWorkloadWithRetry(cfg config) error { + return poll(cfg.maxRetries, cfg.retryInterval, func() (bool, error) { + out, err := runSmokeCapture(cfg.schedulerGRPCAddr, + "-op", "apply-container", + "-workload-id", cfg.testWorkloadID, + "-container-image", "busybox:latest", + "-container-cmd", "sh,-c,sleep 60", + "-w-cpu", "100", + "-w-mem", "128", + "-w-disk", "1", + ) + if err != nil { + msg := strings.ToLower(err.Error()) + if strings.Contains(msg, "no suitable node") || strings.Contains(msg, "cannot place workload") { + return false, nil + } + return false, err + } + lower := strings.ToLower(out) + if strings.Contains(lower, "success=true") { + return true, nil + } + if strings.Contains(lower, "no suitable node") || strings.Contains(lower, "cannot place workload") { + return false, nil + } + return false, fmt.Errorf("apply-container did not report success: %s", out) + }) } -func (suite *TestSuite) testErrorHandling() error { - // Test invalid workload - if err := suite.testInvalidWorkload(); err != nil { - return fmt.Errorf("invalid workload: %w", err) - } +func waitForRegisteredNode(cfg config) error { + logStep("wait for compute-agent node registration") + return poll(cfg.maxRetries, cfg.retryInterval, func() (bool, error) { + out, err := runSmokeCapture(cfg.schedulerGRPCAddr, "-op", "cluster-summary") + if err != nil { + return false, nil + } + var summary struct { + TotalNodes int32 `json:"total_nodes"` + } + if err := unmarshalSmokeJSON(out, &summary); err != nil { + return false, nil + } + return summary.TotalNodes >= 1, nil + }) +} - // Test network failures - if err := suite.testNetworkFailures(); err != nil { - return fmt.Errorf("network failures: %w", err) - } +func waitForWorkloadInList(cfg config) error { + return poll(cfg.maxRetries, cfg.retryInterval, func() (bool, error) { + out, err := runSmokeCapture(cfg.schedulerGRPCAddr, "-op", "list-workloads") + if err != nil { + return false, nil + } + var listResp listWorkloadsResponse + if err := unmarshalSmokeJSON(out, &listResp); err != nil { + return false, nil + } + if containsWorkload(listResp.Workloads, cfg.testWorkloadID) { + return true, nil + } + return false, nil + }) +} - return nil +func runSmoke(schedulerAddr string, args ...string) error { + _, err := runSmokeCapture(schedulerAddr, args...) + return err } -func (suite *TestSuite) testServiceAccessibility() error { - // Test API Gateway - resp, err := suite.httpClient.Get(suite.apiGatewayURL + "/metrics") +func runSmokeCapture(schedulerAddr string, args ...string) (string, error) { + cmdArgs := append([]string{"-scheduler", schedulerAddr}, args...) + ctx, cancel := context.WithTimeout(context.Background(), 60*time.Second) + defer cancel() + + cmd := exec.CommandContext(ctx, "./smoke-client", cmdArgs...) + var stdout, stderr bytes.Buffer + cmd.Stdout = &stdout + cmd.Stderr = &stderr + err := cmd.Run() + out := strings.TrimSpace(stdout.String() + "\n" + stderr.String()) if err != nil { - return fmt.Errorf("API Gateway health check failed: %w", err) + return out, fmt.Errorf("smoke-client %v failed: %w\n%s", args, err, out) } - if resp.StatusCode != 200 { - return fmt.Errorf("API Gateway health check returned status %d", resp.StatusCode) + return out, nil +} + +func waitForHTTP(url string, client *http.Client, retries int, interval time.Duration) error { + for i := 0; i < retries; i++ { + resp, err := client.Get(url) + if err == nil && resp.StatusCode == http.StatusOK { + _ = resp.Body.Close() + return nil + } + if resp != nil { + _ = resp.Body.Close() + } + time.Sleep(interval) } + return fmt.Errorf("timeout waiting for %s", url) +} - // Test Prow Scheduler - resp, err = suite.httpClient.Get(suite.prowSchedulerURL + "/metrics") +func getBody(url string, client *http.Client) (string, error) { + resp, err := client.Get(url) if err != nil { - return fmt.Errorf("Prow Scheduler health check failed: %w", err) + return "", err } - if resp.StatusCode != 200 { - return fmt.Errorf("Prow Scheduler health check returned status %d", resp.StatusCode) + defer resp.Body.Close() + if resp.StatusCode != http.StatusOK { + return "", fmt.Errorf("status: %d", resp.StatusCode) } - - // Test Persys Agent - resp, err = suite.httpClient.Get(suite.persysAgentURL + "/metrics") + b, err := io.ReadAll(resp.Body) if err != nil { - return fmt.Errorf("Persys Agent health check failed: %w", err) - } - if resp.StatusCode != 200 { - return fmt.Errorf("Persys Agent health check returned status %d", resp.StatusCode) + return "", err } - - return nil + return string(b), nil } -func (suite *TestSuite) testWorkloadCreation() error { - // workload := map[string]interface{}{ - // "name": "test-nginx", - // "type": "docker-container", - // "image": "nginx:alpine", - // "ports": []string{"8080:80"}, - // } - - // // Create workload via API Gateway - // resp, err := suite.httpClient.PostJSON(suite.apiGatewayURL+"/workloads/schedule", workload) - // if err != nil { - // return fmt.Errorf("failed to create workload: %w", err) - // } - // if resp.StatusCode != 201 { - // return fmt.Errorf("workload creation returned status %d", resp.StatusCode) - // } - - // var result map[string]interface{} - // err = json.NewDecoder(resp.Body).Decode(&result) - // if err != nil { - // return fmt.Errorf("failed to decode response: %w", err) - // } - // if result["workloadId"] == "" { - // return fmt.Errorf("workload ID is empty") - // } - - return nil -} - -func (suite *TestSuite) testWorkloadExecution() error { - // // Wait for workload to be executed - // time.Sleep(10 * time.Second) - - // // Check if container is running - // resp, err := suite.httpClient.Get(suite.persysAgentURL + "/api/v1/containers") - // if err != nil { - // return fmt.Errorf("failed to get containers: %w", err) - // } - // if resp.StatusCode != 200 { - // return fmt.Errorf("container list returned status %d", resp.StatusCode) - // } - - // var containers []map[string]interface{} - // err = json.NewDecoder(resp.Body).Decode(&containers) - // if err != nil { - // return fmt.Errorf("failed to decode containers: %w", err) - // } - - // // Verify test container is running - // found := false - // for _, container := range containers { - // if name, ok := container["name"].(string); ok && name == "test-nginx" { - // found = true - // if container["status"] != "running" { - // return fmt.Errorf("test container status is %v, expected running", container["status"]) - // } - // break - // } - // } - // if !found { - // return fmt.Errorf("test container not found") - // } - - return nil +func poll(retries int, interval time.Duration, f func() (bool, error)) error { + for i := 0; i < retries; i++ { + ok, err := f() + if err != nil { + return err + } + if ok { + return nil + } + time.Sleep(interval) + } + return fmt.Errorf("timeout while polling condition") } -func (suite *TestSuite) testMonitoring() error { - // Check metrics endpoint - resp, err := suite.httpClient.Get(suite.apiGatewayURL + "/metrics") +func unmarshalSmokeJSON(out string, target interface{}) error { + raw, err := extractJSONObject(out) if err != nil { - return fmt.Errorf("metrics endpoint failed: %w", err) - } - if resp.StatusCode != 200 { - return fmt.Errorf("metrics endpoint returned status %d", resp.StatusCode) + return err } - - // // Check workload status - // resp, err = suite.httpClient.Get(suite.apiGatewayURL + "/workloads") - // if err != nil { - // return fmt.Errorf("workload status failed: %w", err) - // } - // if resp.StatusCode != 200 { - // return fmt.Errorf("workload status returned status %d", resp.StatusCode) - // } - - return nil -} - -func (suite *TestSuite) testMTLSAuthentication() error { - // Test with valid mTLS certificates - // This would require proper certificate setup - fmt.Println(" Testing mTLS authentication...") - return nil -} - -func (suite *TestSuite) testHMACAuthentication() error { - // Test HMAC between prow and persys-agent - fmt.Println(" Testing HMAC authentication...") - return nil -} - -func (suite *TestSuite) testInvalidAuthentication() error { - // Test with invalid credentials - fmt.Println(" Testing invalid authentication...") - return nil -} - -func (suite *TestSuite) testSchedulerDiscovery() error { - // Test CoreDNS-based discovery - fmt.Println(" Testing scheduler discovery...") - return nil + return json.Unmarshal([]byte(raw), target) } -func (suite *TestSuite) testDiscoveryFallback() error { - // Test fallback to configured address - fmt.Println(" Testing discovery fallback...") - return nil -} - -func (suite *TestSuite) testImmediateResponse() error { - // Test that workload creation returns immediately - // start := time.Now() - - // workload := map[string]interface{}{ - // "name": "async-test", - // "type": "docker-container", - // "image": "nginx:alpine", - // } - - // resp, err := suite.httpClient.PostJSON(suite.apiGatewayURL+"/workloads", workload) - // if err != nil { - // return fmt.Errorf("failed to create async workload: %w", err) - // } - // if resp.StatusCode != 201 { - // return fmt.Errorf("async workload creation returned status %d", resp.StatusCode) - // } - - // duration := time.Since(start) - // if duration > 5*time.Second { - // return fmt.Errorf("response took %v, should be immediate", duration) - // } - - return nil +func extractJSONObject(s string) (string, error) { + start := strings.Index(s, "{") + end := strings.LastIndex(s, "}") + if start == -1 || end == -1 || end <= start { + return "", fmt.Errorf("no JSON object found in output: %s", s) + } + return s[start : end+1], nil } -func (suite *TestSuite) testBackgroundExecution() error { - // Test that workload executes in background - fmt.Println(" Testing background execution...") - return nil +func containsWorkload(workloads []workloadView, workloadID string) bool { + for _, w := range workloads { + if strings.TrimSpace(w.WorkloadID) == workloadID { + return true + } + } + return false } -func (suite *TestSuite) testContainerRestart() error { - // Test reconciliation by stopping container - fmt.Println(" Testing container restart...") - return nil +func getenvIntDefault(key string, fallback int) int { + v := strings.TrimSpace(os.Getenv(key)) + if v == "" { + return fallback + } + n, err := strconv.Atoi(v) + if err != nil { + return fallback + } + return n } -func (suite *TestSuite) testReconciliationStats() error { - // Check reconciliation statistics - // Test CoreDNS-based discovery - fmt.Println(" Testing reconciliation ...") - return nil - +func getenvDurationDefault(key string, fallback time.Duration) time.Duration { + v := strings.TrimSpace(os.Getenv(key)) + if v == "" { + return fallback + } + if d, err := time.ParseDuration(v); err == nil { + return d + } + if s, err := strconv.Atoi(v); err == nil { + return time.Duration(s) * time.Second + } + return fallback } -func (suite *TestSuite) testInvalidWorkload() error { - // // Test with invalid workload configuration - // invalidWorkload := map[string]interface{}{ - // "name": "invalid", - // "type": "invalid-type", - // } - - // resp, err := suite.httpClient.PostJSON(suite.apiGatewayURL+"/workloads/schedule", invalidWorkload) - // if err != nil { - // return fmt.Errorf("failed to submit invalid workload: %w", err) - // } - // if resp.StatusCode != 400 { - // return fmt.Errorf("invalid workload returned status %d, expected 400", resp.StatusCode) - // } - - return nil +func getenvDefault(key, fallback string) string { + if v := strings.TrimSpace(os.Getenv(key)); v != "" { + return v + } + return fallback } -func (suite *TestSuite) testNetworkFailures() error { - // Test network failure scenarios - fmt.Println(" Testing network failures...") - return nil +func logStep(msg string) { + log.Printf("==> %s", msg) } -// PostJSON sends a POST request with JSON data -func (c *HTTPClient) PostJSON(url string, data interface{}) (*http.Response, error) { - jsonData, err := json.Marshal(data) - if err != nil { - return nil, err - } - - req, err := http.NewRequest("POST", url, bytes.NewBuffer(jsonData)) +func must(err error) { if err != nil { - return nil, err + failf("%v", err) } +} - req.Header.Set("Content-Type", "application/json") - return c.Do(req) +func failf(format string, args ...interface{}) { + log.Fatalf(format, args...) } diff --git a/tests/e2e/test-suite.sh b/tests/e2e/test-suite.sh deleted file mode 100644 index bd676ba..0000000 --- a/tests/e2e/test-suite.sh +++ /dev/null @@ -1,309 +0,0 @@ -#!/bin/bash - -set -e - -# Colors for output -RED='\033[0;31m' -GREEN='\033[0;32m' -YELLOW='\033[1;33m' -BLUE='\033[0;34m' -NC='\033[0m' # No Color - -# Configuration -API_GATEWAY_URL="${API_GATEWAY_URL:-https://localhost:8551}" -PROW_SCHEDULER_URL="${PROW_SCHEDULER_URL:-https://localhost:8085}" -PERSYS_AGENT_URL="${PERSYS_AGENT_URL:-http://localhost:8080}" -TEST_TIMEOUT=30 -RETRY_INTERVAL=2 - -# Test results -PASSED=0 -FAILED=0 -TOTAL=0 - -# Helper functions -log_info() { - echo -e "${BLUE}ℹ️ $1${NC}" -} - -log_success() { - echo -e "${GREEN}✅ $1${NC}" -} - -log_error() { - echo -e "${RED}❌ $1${NC}" -} - -log_warning() { - echo -e "${YELLOW}⚠️ $1${NC}" -} - -# Wait for service to be ready -wait_for_service() { - local name=$1 - local url=$2 - local max_retries=30 - - log_info "Waiting for $name to be ready..." - - for i in $(seq 1 $max_retries); do - if curl -s -f "$url/health" > /dev/null 2>&1; then - log_success "$name is ready" - return 0 - fi - sleep $RETRY_INTERVAL - done - - log_error "$name failed to start" - return 1 -} - -# Run a test -run_test() { - local test_name=$1 - local test_func=$2 - - TOTAL=$((TOTAL + 1)) - log_info "Running test: $test_name" - - local start_time=$(date +%s) - - if $test_func; then - local end_time=$(date +%s) - local duration=$((end_time - start_time)) - log_success "$test_name passed (${duration}s)" - PASSED=$((PASSED + 1)) - else - local end_time=$(date +%s) - local duration=$((end_time - start_time)) - log_error "$test_name failed (${duration}s)" - FAILED=$((FAILED + 1)) - fi -} - -# Test functions - -test_service_health() { - log_info "Testing service health endpoints..." - - # Test API Gateway - if ! curl -s -f "$API_GATEWAY_URL/health" > /dev/null; then - log_error "API Gateway health check failed" - return 1 - fi - - # Test Prow Scheduler - if ! curl -s -f "$PROW_SCHEDULER_URL/health" > /dev/null; then - log_error "Prow Scheduler health check failed" - return 1 - fi - - # Test Persys Agent - if ! curl -s -f "$PERSYS_AGENT_URL/health" > /dev/null; then - log_error "Persys Agent health check failed" - return 1 - fi - - log_success "All services are healthy" - return 0 -} - -test_workload_creation() { - log_info "Testing workload creation..." - - local workload_data='{ - "name": "test-nginx", - "type": "docker-container", - "image": "nginx:alpine", - "ports": ["8080:80"] - }' - - local response=$(curl -s -w "%{http_code}" -X POST \ - -H "Content-Type: application/json" \ - -d "$workload_data" \ - "$API_GATEWAY_URL/api/v1/workloads") - - local status_code="${response: -3}" - local response_body="${response%???}" - - if [ "$status_code" != "201" ]; then - log_error "Workload creation failed with status $status_code: $response_body" - return 1 - fi - - # Extract workload ID from response - local workload_id=$(echo "$response_body" | jq -r '.workloadId // empty') - if [ -z "$workload_id" ]; then - log_error "No workload ID in response" - return 1 - fi - - log_success "Workload created with ID: $workload_id" - return 0 -} - -test_workload_execution() { - log_info "Testing workload execution..." - - # Wait for workload to be executed - sleep 10 - - # Check if container is running - local containers=$(curl -s "$PERSYS_AGENT_URL/api/v1/containers") - local test_container=$(echo "$containers" | jq -r '.[] | select(.name == "test-nginx") | .status') - - if [ "$test_container" != "running" ]; then - log_error "Test container is not running (status: $test_container)" - return 1 - fi - - log_success "Test container is running" - return 0 -} - -test_metrics_endpoint() { - log_info "Testing metrics endpoint..." - - if ! curl -s -f "$API_GATEWAY_URL/metrics" > /dev/null; then - log_error "Metrics endpoint failed" - return 1 - fi - - log_success "Metrics endpoint is accessible" - return 0 -} - -test_workload_listing() { - log_info "Testing workload listing..." - - local workloads=$(curl -s "$API_GATEWAY_URL/api/v1/workloads") - local workload_count=$(echo "$workloads" | jq '. | length') - - if [ "$workload_count" -eq 0 ]; then - log_warning "No workloads found" - else - log_success "Found $workload_count workload(s)" - fi - - return 0 -} - -test_reconciliation_stats() { - log_info "Testing reconciliation statistics..." - - if ! curl -s -f "$PROW_SCHEDULER_URL/api/v1/reconciliation/stats" > /dev/null; then - log_error "Reconciliation stats endpoint failed" - return 1 - fi - - log_success "Reconciliation stats are accessible" - return 0 -} - -test_invalid_workload() { - log_info "Testing invalid workload rejection..." - - local invalid_workload='{ - "name": "invalid", - "type": "invalid-type" - }' - - local response=$(curl -s -w "%{http_code}" -X POST \ - -H "Content-Type: application/json" \ - -d "$invalid_workload" \ - "$API_GATEWAY_URL/api/v1/workloads") - - local status_code="${response: -3}" - - if [ "$status_code" != "400" ]; then - log_error "Invalid workload was not rejected (status: $status_code)" - return 1 - fi - - log_success "Invalid workload properly rejected" - return 0 -} - -test_async_execution() { - log_info "Testing async execution..." - - local start_time=$(date +%s) - - local workload_data='{ - "name": "async-test", - "type": "docker-container", - "image": "nginx:alpine" - }' - - local response=$(curl -s -w "%{http_code}" -X POST \ - -H "Content-Type: application/json" \ - -d "$workload_data" \ - "$API_GATEWAY_URL/api/v1/workloads") - - local end_time=$(date +%s) - local duration=$((end_time - start_time)) - - if [ "$duration" -gt 5 ]; then - log_error "Async execution took too long (${duration}s)" - return 1 - fi - - log_success "Async execution responded quickly (${duration}s)" - return 0 -} - -# Main test execution -main() { - echo "🚀 Starting Persys Cloud E2E Tests..." - echo "======================================" - - # Wait for services to be ready - wait_for_service "API Gateway" "$API_GATEWAY_URL" - wait_for_service "Prow Scheduler" "$PROW_SCHEDULER_URL" - wait_for_service "Persys Agent" "$PERSYS_AGENT_URL" - - echo "" - echo "📋 Running test scenarios..." - echo "============================" - - # Run tests - run_test "Service Health" test_service_health - run_test "Workload Creation" test_workload_creation - run_test "Workload Execution" test_workload_execution - run_test "Metrics Endpoint" test_metrics_endpoint - run_test "Workload Listing" test_workload_listing - run_test "Reconciliation Stats" test_reconciliation_stats - run_test "Invalid Workload" test_invalid_workload - run_test "Async Execution" test_async_execution - - echo "" - echo "📊 Test Results" - echo "===============" - echo "Total tests: $TOTAL" - echo "Passed: $PASSED" - echo "Failed: $FAILED" - - if [ $FAILED -eq 0 ]; then - log_success "All tests passed! 🎉" - exit 0 - else - log_error "Some tests failed! 💥" - exit 1 - fi -} - -# Check dependencies -check_dependencies() { - local deps=("curl" "jq") - - for dep in "${deps[@]}"; do - if ! command -v "$dep" &> /dev/null; then - log_error "Required dependency '$dep' is not installed" - exit 1 - fi - done -} - -# Run main function -check_dependencies -main \ No newline at end of file diff --git a/tests/integration/go.mod b/tests/integration/go.mod new file mode 100644 index 0000000..2228e1e --- /dev/null +++ b/tests/integration/go.mod @@ -0,0 +1,13 @@ +module persys-cloud-integration + +go 1.21 + +require ( + github.com/google/uuid v1.6.0 + github.com/redis/go-redis/v9 v9.11.0 +) + +require ( + github.com/cespare/xxhash/v2 v2.3.0 // indirect + github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f // indirect +) diff --git a/tests/integration/go.sum b/tests/integration/go.sum new file mode 100644 index 0000000..7d45fdb --- /dev/null +++ b/tests/integration/go.sum @@ -0,0 +1,12 @@ +github.com/bsm/ginkgo/v2 v2.12.0 h1:Ny8MWAHyOepLGlLKYmXG4IEkioBysk6GpaRTLC8zwWs= +github.com/bsm/ginkgo/v2 v2.12.0/go.mod h1:SwYbGRRDovPVboqFv0tPTcG1sN61LM1Z4ARdbAV9g4c= +github.com/bsm/gomega v1.27.10 h1:yeMWxP2pV2fG3FgAODIY8EiRE3dy0aeFYt4l7wh6yKA= +github.com/bsm/gomega v1.27.10/go.mod h1:JyEr/xRbxbtgWNi8tIEVPUYZ5Dzef52k01W3YH0H+O0= +github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs= +github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= +github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f h1:lO4WD4F/rVNCu3HqELle0jiPLLBs70cWOduZpkS1E78= +github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f/go.mod h1:cuUVRXasLTGF7a8hSLbxyZXjz+1KgoB3wDUb6vlszIc= +github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= +github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= +github.com/redis/go-redis/v9 v9.11.0 h1:E3S08Gl/nJNn5vkxd2i78wZxWAPNZgUNTp8WIJUAiIs= +github.com/redis/go-redis/v9 v9.11.0/go.mod h1:huWgSWd8mW6+m0VPhJjSSQ+d6Nh1VICQ6Q5lHuCH/Iw= diff --git a/tests/integration/test-model-compatibility.go b/tests/integration/test-model-compatibility.go new file mode 100644 index 0000000..1aec567 --- /dev/null +++ b/tests/integration/test-model-compatibility.go @@ -0,0 +1,143 @@ +package integration + +import ( + "encoding/json" + "fmt" + "log" + "time" + + "github.com/google/uuid" +) + +// Prow's BuildQueueMessage (what gets sent to Redis) +type BuildQueueMessage struct { + ID string `json:"id"` + ProjectName string `json:"project_name"` + Type string `json:"type"` + Source string `json:"source"` + CommitHash string `json:"commit_hash"` + Branch string `json:"branch"` + Strategy string `json:"strategy"` + PushArtifact bool `json:"push_artifact"` + WebhookData map[string]interface{} `json:"webhook_data"` + Metadata map[string]interface{} `json:"metadata"` + CreatedAt time.Time `json:"created_at"` +} + +// Forgery's BuildRequest (what gets consumed from Redis) +type BuildRequest struct { + ID string `json:"id,omitempty"` + ProjectName string `json:"project_name"` + Type string `json:"type"` + Source string `json:"source"` + CommitHash string `json:"commit_hash"` + Branch string `json:"branch,omitempty"` + Pipeline string `json:"pipeline,omitempty"` + Strategy string `json:"strategy"` + PushArtifact bool `json:"push_artifact,omitempty"` + NexusRepo string `json:"nexus_repo,omitempty"` + WebhookData map[string]interface{} `json:"webhook_data,omitempty"` + Metadata map[string]interface{} `json:"metadata,omitempty"` + CreatedAt time.Time `json:"created_at,omitempty"` +} + +func IntegrationTest() { + fmt.Println("Testing Prow-Forgery Model Compatibility") + fmt.Println("========================================") + + // Create a test message from Prow + prowMessage := BuildQueueMessage{ + ID: uuid.New().String(), + ProjectName: "test-org/test-repo", + Type: "dockerfile", + Source: "https://github.com/test-org/test-repo.git", + CommitHash: "abc123def456", + Branch: "main", + Strategy: "local", + PushArtifact: true, + WebhookData: map[string]interface{}{ + "event_type": "push", + "repository": "test-org/test-repo", + "sender": "testuser", + "ref": "refs/heads/main", + "before": "old-commit", + "after": "abc123def456", + }, + Metadata: map[string]interface{}{ + "triggered_by": "webhook", + "correlation_id": uuid.New().String(), + "processed_by": "prow", + }, + CreatedAt: time.Now(), + } + + fmt.Printf("Prow Message:\n") + fmt.Printf(" ID: %s\n", prowMessage.ID) + fmt.Printf(" Project: %s\n", prowMessage.ProjectName) + fmt.Printf(" Type: %s\n", prowMessage.Type) + fmt.Printf(" Source: %s\n", prowMessage.Source) + fmt.Printf(" Commit: %s\n", prowMessage.CommitHash) + fmt.Printf(" Branch: %s\n", prowMessage.Branch) + fmt.Printf(" Strategy: %s\n", prowMessage.Strategy) + fmt.Printf(" PushArtifact: %t\n", prowMessage.PushArtifact) + + // Marshal Prow message to JSON + prowJSON, err := json.Marshal(prowMessage) + if err != nil { + log.Fatalf("Failed to marshal Prow message: %v", err) + } + + fmt.Printf("\nProw JSON:\n%s\n", string(prowJSON)) + + // Unmarshal into Forgery's BuildRequest + var forgeryRequest BuildRequest + if err := json.Unmarshal(prowJSON, &forgeryRequest); err != nil { + log.Fatalf("Failed to unmarshal to Forgery model: %v", err) + } + + fmt.Printf("\nForgery Request (unmarshaled):\n") + fmt.Printf(" ID: %s\n", forgeryRequest.ID) + fmt.Printf(" Project: %s\n", forgeryRequest.ProjectName) + fmt.Printf(" Type: %s\n", forgeryRequest.Type) + fmt.Printf(" Source: %s\n", forgeryRequest.Source) + fmt.Printf(" Commit: %s\n", forgeryRequest.CommitHash) + fmt.Printf(" Branch: %s\n", forgeryRequest.Branch) + fmt.Printf(" Strategy: %s\n", forgeryRequest.Strategy) + fmt.Printf(" PushArtifact: %t\n", forgeryRequest.PushArtifact) + fmt.Printf(" Webhook Event: %s\n", forgeryRequest.WebhookData["event_type"]) + fmt.Printf(" Correlation ID: %s\n", forgeryRequest.Metadata["correlation_id"]) + + // Test reverse compatibility (Forgery -> Prow) + forgeryJSON, err := json.Marshal(forgeryRequest) + if err != nil { + log.Fatalf("Failed to marshal Forgery request: %v", err) + } + + var backToProw BuildQueueMessage + if err := json.Unmarshal(forgeryJSON, &backToProw); err != nil { + log.Fatalf("Failed to unmarshal back to Prow model: %v", err) + } + + fmt.Printf("\nRound-trip compatibility test:\n") + fmt.Printf(" Original ID: %s\n", prowMessage.ID) + fmt.Printf(" Round-trip ID: %s\n", backToProw.ID) + fmt.Printf(" IDs match: %t\n", prowMessage.ID == backToProw.ID) + + // Check if all critical fields are preserved + criticalFieldsMatch := + prowMessage.ProjectName == backToProw.ProjectName && + prowMessage.Type == backToProw.Type && + prowMessage.Source == backToProw.Source && + prowMessage.CommitHash == backToProw.CommitHash && + prowMessage.Strategy == backToProw.Strategy + + fmt.Printf(" Critical fields match: %t\n", criticalFieldsMatch) + + if criticalFieldsMatch { + fmt.Println("\n✅ Model compatibility test PASSED!") + fmt.Println("Prow and Forgery models are fully compatible.") + } else { + fmt.Println("\n❌ Model compatibility test FAILED!") + fmt.Println("Some fields were lost during round-trip conversion.") + } +} diff --git a/tests/integration/test-redis-communication.go b/tests/integration/test-redis-communication.go new file mode 100644 index 0000000..7fd09a5 --- /dev/null +++ b/tests/integration/test-redis-communication.go @@ -0,0 +1,95 @@ +package integration + +import ( + "context" + "encoding/json" + "fmt" + "log" + "time" + + "github.com/redis/go-redis/v9" +) + +func main() { + // Connect to Redis + rdb := redis.NewClient(&redis.Options{ + Addr: "localhost:6379", + Password: "", + DB: 0, + }) + + ctx := context.Background() + + // Test message from Prow + testMessage := BuildQueueMessage{ + ID: "test-build-123", + ProjectName: "test-org/test-repo", + Type: "dockerfile", + Source: "https://github.com/test-org/test-repo.git", + CommitHash: "abc123def456", + Branch: "main", + Strategy: "local", + PushArtifact: true, + WebhookData: map[string]interface{}{ + "event_type": "push", + "repository": "test-org/test-repo", + "sender": "testuser", + "ref": "refs/heads/main", + "before": "old-commit", + "after": "abc123def456", + }, + Metadata: map[string]interface{}{ + "triggered_by": "webhook", + "correlation_id": "correlation-123", + "processed_by": "prow", + }, + CreatedAt: time.Now(), + } + + // Marshal to JSON + messageJSON, err := json.Marshal(testMessage) + if err != nil { + log.Fatalf("Failed to marshal message: %v", err) + } + + fmt.Printf("Sending message to Redis:\n%s\n", string(messageJSON)) + + // Push to Redis queue + err = rdb.LPush(ctx, "forge:builds", messageJSON).Err() + if err != nil { + log.Fatalf("Failed to push to Redis: %v", err) + } + + fmt.Println("Message sent to Redis queue 'forge:builds'") + + // Test consuming the message (simulating Forgery) + fmt.Println("\nTesting message consumption...") + res, err := rdb.BLPop(ctx, 5*time.Second, "forge:builds").Result() + if err != nil { + log.Fatalf("Failed to consume from Redis: %v", err) + } + + if len(res) < 2 { + log.Fatal("Unexpected response format") + } + + fmt.Printf("Received message: %s\n", res[1]) + + // Test unmarshaling into BuildRequest (Forgery's model) + var receivedMessage BuildQueueMessage + if err := json.Unmarshal([]byte(res[1]), &receivedMessage); err != nil { + log.Fatalf("Failed to unmarshal received message: %v", err) + } + + fmt.Printf("Successfully unmarshaled message:\n") + fmt.Printf(" ID: %s\n", receivedMessage.ID) + fmt.Printf(" Project: %s\n", receivedMessage.ProjectName) + fmt.Printf(" Type: %s\n", receivedMessage.Type) + fmt.Printf(" Source: %s\n", receivedMessage.Source) + fmt.Printf(" Commit: %s\n", receivedMessage.CommitHash) + fmt.Printf(" Branch: %s\n", receivedMessage.Branch) + fmt.Printf(" Strategy: %s\n", receivedMessage.Strategy) + fmt.Printf(" Webhook Event: %s\n", receivedMessage.WebhookData["event_type"]) + + fmt.Println("\n✅ Redis communication test successful!") +}