Files
vm/service/doks.go
zooqueenandHanzo Dev a121f8d0d0 refactor: rebrand from casvisor to hanzoai/visor, remove hardcoded credentials
- Rename Go module from github.com/casvisor/casvisor to github.com/hanzoai/visor
- Update all copyright headers from casbin to Hanzo Industries Inc
- Remove all hardcoded passwords and credentials from config files
- All credentials now sourced from environment variables / Hanzo KMS
- Switch default database from MySQL to PostgreSQL (matching production)
- Rename binary output from server/casvisor to visor
- Update Dockerfile, build.sh, docker-compose.yml, docker-entrypoint.sh
- Replace casdoor* config key references with iam* equivalents
- Update CI to use PostgreSQL service and mark CI-only test password
- Add visor and casvisor binaries to .gitignore
- Add envFrom secretRef for visor-env in K8s deployment
- Add ExternalSecret template for Hanzo KMS credential provisioning

Co-authored-by: Hanzo Dev <[email protected]>
2026-02-21 00:04:31 -08:00

204 lines
5.6 KiB
Go

// Copyright 2025 Hanzo Industries Inc. All Rights Reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package service
import (
"context"
"fmt"
"github.com/digitalocean/godo"
"golang.org/x/oauth2"
)
type NodeInfo struct {
ID string `json:"id"`
Name string `json:"name"`
Status string `json:"status"`
DropletID string `json:"dropletId"`
CreatedAt string `json:"createdAt"`
UpdatedAt string `json:"updatedAt"`
}
type NodePool struct {
ID string `json:"id"`
Name string `json:"name"`
Size string `json:"size"`
Count int `json:"count"`
MinNodes int `json:"minNodes"`
MaxNodes int `json:"maxNodes"`
AutoScale bool `json:"autoScale"`
Nodes []NodeInfo `json:"nodes"`
Tags []string `json:"tags"`
Labels map[string]string `json:"labels"`
}
type CreateNodePoolSpec struct {
Name string `json:"name"`
Size string `json:"size"`
Count int `json:"count"`
MinNodes int `json:"minNodes"`
MaxNodes int `json:"maxNodes"`
AutoScale bool `json:"autoScale"`
Tags []string `json:"tags,omitempty"`
Labels map[string]string `json:"labels,omitempty"`
}
type DOKSClient struct {
Client *godo.Client
ClusterID string
}
func NewDOKSClient(token, clusterID string) (*DOKSClient, error) {
if token == "" {
return nil, fmt.Errorf("DigitalOcean API token is required")
}
if clusterID == "" {
return nil, fmt.Errorf("DOKS cluster ID is required")
}
tokenSource := oauth2.StaticTokenSource(&oauth2.Token{AccessToken: token})
oauthClient := oauth2.NewClient(context.Background(), tokenSource)
client := godo.NewClient(oauthClient)
return &DOKSClient{Client: client, ClusterID: clusterID}, nil
}
func nodePoolFromGodo(pool *godo.KubernetesNodePool) *NodePool {
np := &NodePool{
ID: pool.ID,
Name: pool.Name,
Size: pool.Size,
Count: pool.Count,
MinNodes: pool.MinNodes,
MaxNodes: pool.MaxNodes,
AutoScale: pool.AutoScale,
Tags: pool.Tags,
Labels: pool.Labels,
}
for _, node := range pool.Nodes {
ni := NodeInfo{
ID: node.ID,
Name: node.Name,
DropletID: node.DropletID,
}
if node.Status != nil {
ni.Status = node.Status.State
}
if !node.CreatedAt.IsZero() {
ni.CreatedAt = node.CreatedAt.Format("2006-01-02T15:04:05Z")
}
if !node.UpdatedAt.IsZero() {
ni.UpdatedAt = node.UpdatedAt.Format("2006-01-02T15:04:05Z")
}
np.Nodes = append(np.Nodes, ni)
}
return np
}
func (c *DOKSClient) ListNodePools() ([]*NodePool, error) {
opt := &godo.ListOptions{Page: 1, PerPage: 200}
var allPools []*NodePool
for {
pools, resp, err := c.Client.Kubernetes.ListNodePools(context.TODO(), c.ClusterID, opt)
if err != nil {
return nil, fmt.Errorf("failed to list node pools: %w", err)
}
for _, pool := range pools {
allPools = append(allPools, nodePoolFromGodo(pool))
}
if resp.Links == nil || resp.Links.IsLastPage() {
break
}
opt.Page++
}
return allPools, nil
}
func (c *DOKSClient) GetNodePool(poolID string) (*NodePool, error) {
pool, _, err := c.Client.Kubernetes.GetNodePool(context.TODO(), c.ClusterID, poolID)
if err != nil {
return nil, fmt.Errorf("failed to get node pool %s: %w", poolID, err)
}
return nodePoolFromGodo(pool), nil
}
func (c *DOKSClient) CreateNodePool(spec *CreateNodePoolSpec) (*NodePool, error) {
req := &godo.KubernetesNodePoolCreateRequest{
Name: spec.Name,
Size: spec.Size,
Count: spec.Count,
MinNodes: spec.MinNodes,
MaxNodes: spec.MaxNodes,
AutoScale: spec.AutoScale,
Tags: spec.Tags,
Labels: spec.Labels,
}
pool, _, err := c.Client.Kubernetes.CreateNodePool(context.TODO(), c.ClusterID, req)
if err != nil {
return nil, fmt.Errorf("failed to create node pool: %w", err)
}
return nodePoolFromGodo(pool), nil
}
func (c *DOKSClient) UpdateNodePool(poolID string, spec *CreateNodePoolSpec) (*NodePool, error) {
req := &godo.KubernetesNodePoolUpdateRequest{
Name: spec.Name,
Count: &spec.Count,
MinNodes: &spec.MinNodes,
MaxNodes: &spec.MaxNodes,
AutoScale: &spec.AutoScale,
Tags: spec.Tags,
Labels: spec.Labels,
}
pool, _, err := c.Client.Kubernetes.UpdateNodePool(context.TODO(), c.ClusterID, poolID, req)
if err != nil {
return nil, fmt.Errorf("failed to update node pool %s: %w", poolID, err)
}
return nodePoolFromGodo(pool), nil
}
func (c *DOKSClient) DeleteNodePool(poolID string) error {
_, err := c.Client.Kubernetes.DeleteNodePool(context.TODO(), c.ClusterID, poolID)
if err != nil {
return fmt.Errorf("failed to delete node pool %s: %w", poolID, err)
}
return nil
}
func (c *DOKSClient) RecycleNodePoolNodes(poolID string, nodeIDs []string) error {
req := &godo.KubernetesNodePoolRecycleNodesRequest{
Nodes: nodeIDs,
}
_, err := c.Client.Kubernetes.RecycleNodePoolNodes(context.TODO(), c.ClusterID, poolID, req)
if err != nil {
return fmt.Errorf("failed to recycle nodes in pool %s: %w", poolID, err)
}
return nil
}