Files
zeekayandHanzo Dev 03fcdb55d6 Go 1.27.1, and the code it can say more simply
Modules declare go 1.27.1 and the build images follow. go fix applied the
modernizers; omitzero is not applied, since it changes what the JSON says.


Co-authored-by: Hanzo Dev <[email protected]>
2026-09-16 19:27:30 -07:00

223 lines
6.8 KiB
Go

// Copyright 2024 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"
"encoding/json"
"fmt"
"io"
"net/http"
"net/url"
"golang.org/x/oauth2"
"golang.org/x/oauth2/google"
)
const (
computeBaseURL = "https://compute.googleapis.com/compute/v1"
computeScope = "https://www.googleapis.com/auth/compute"
)
type MachineGcpClient struct {
httpClient *http.Client
projectID string
zone string
}
// computeInstance is the subset of the compute REST instance resource this
// adapter maps. JSON over HTTP — no SDK, no gRPC.
type computeInstance struct {
Name string `json:"name"`
Id string `json:"id"`
CreationTimestamp string `json:"creationTimestamp"`
LastStartTimestamp string `json:"lastStartTimestamp"`
Zone string `json:"zone"`
MachineType string `json:"machineType"`
Status string `json:"status"`
SourceMachineImage string `json:"sourceMachineImage"`
CpuPlatform string `json:"cpuPlatform"`
Labels map[string]string `json:"labels"`
Disks []computeDisk `json:"disks"`
NetworkInterfaces []computeNetworkIface `json:"networkInterfaces"`
}
type computeDisk struct {
GuestOsFeatures []computeGuestOsFeature `json:"guestOsFeatures"`
}
type computeGuestOsFeature struct {
Type string `json:"type"`
}
type computeNetworkIface struct {
NetworkIP string `json:"networkIP"`
AccessConfigs []computeAccessConfig `json:"accessConfigs"`
}
type computeAccessConfig struct {
NatIP string `json:"natIP"`
}
type computeInstanceList struct {
Items []computeInstance `json:"items"`
}
// tokenSource returns an OAuth2 token source over pure HTTP token exchange:
// the service-account JSON when supplied, else Application Default Credentials
// (in-cluster workload identity). No gRPC is involved.
func gcpTokenSource(ctx context.Context, credentialsJSON string) (oauth2.TokenSource, error) {
if credentialsJSON != "" {
cfg, err := google.JWTConfigFromJSON([]byte(credentialsJSON), computeScope)
if err != nil {
return nil, err
}
return cfg.TokenSource(ctx), nil
}
return google.DefaultTokenSource(ctx, computeScope)
}
func newMachineGcpClient(projectID string, credentialsJSON string, zone string) (MachineGcpClient, error) {
ctx := context.Background()
ts, err := gcpTokenSource(ctx, credentialsJSON)
if err != nil {
return MachineGcpClient{}, err
}
return MachineGcpClient{httpClient: oauth2.NewClient(ctx, ts), projectID: projectID, zone: zone}, nil
}
// do issues an authenticated JSON request and decodes a 2xx body into out
// (out may be nil). A non-2xx response is a hard error with a bounded body.
func (client MachineGcpClient) do(ctx context.Context, method string, endpoint string, out any) error {
req, err := http.NewRequestWithContext(ctx, method, endpoint, nil)
if err != nil {
return err
}
resp, err := client.httpClient.Do(req)
if err != nil {
return err
}
defer resp.Body.Close()
if resp.StatusCode < 200 || resp.StatusCode >= 300 {
body, _ := io.ReadAll(io.LimitReader(resp.Body, 4096))
return fmt.Errorf("compute API %s %s: %s", method, resp.Status, string(body))
}
if out != nil {
return json.NewDecoder(resp.Body).Decode(out)
}
return nil
}
func (client MachineGcpClient) instancesURL() string {
return fmt.Sprintf("%s/projects/%s/zones/%s/instances", computeBaseURL, client.projectID, client.zone)
}
func getMachineFromComputeInstance(instance *computeInstance) *Machine {
machine := &Machine{
Name: instance.Name,
Id: instance.Id,
CreatedTime: getLocalTimestamp(instance.CreationTimestamp),
UpdatedTime: getLocalTimestamp(instance.LastStartTimestamp),
DisplayName: instance.Name,
Zone: instance.Zone,
Type: instance.MachineType,
State: instance.Status,
Image: instance.SourceMachineImage,
CpuSize: instance.CpuPlatform,
}
if len(instance.Disks) > 0 && len(instance.Disks[0].GuestOsFeatures) > 0 {
machine.Os = instance.Disks[0].GuestOsFeatures[0].Type
}
for key, value := range instance.Labels {
machine.Tag += fmt.Sprintf("%s=%s,", key, value)
}
if len(instance.NetworkInterfaces) > 0 {
if len(instance.NetworkInterfaces[0].AccessConfigs) > 0 {
machine.PublicIp = instance.NetworkInterfaces[0].AccessConfigs[0].NatIP
}
machine.PrivateIp = instance.NetworkInterfaces[0].NetworkIP
}
return machine
}
func (client MachineGcpClient) GetMachines() ([]*Machine, error) {
ctx := context.Background()
endpoint := fmt.Sprintf("%s?maxResults=100", client.instancesURL())
var list computeInstanceList
if err := client.do(ctx, http.MethodGet, endpoint, &list); err != nil {
return nil, err
}
machines := []*Machine{}
for i := range list.Items {
machines = append(machines, getMachineFromComputeInstance(&list.Items[i]))
}
return machines, nil
}
func (client MachineGcpClient) GetMachine(name string) (*Machine, error) {
ctx := context.Background()
endpoint := fmt.Sprintf("%s/%s", client.instancesURL(), url.PathEscape(name))
var instance computeInstance
if err := client.do(ctx, http.MethodGet, endpoint, &instance); err != nil {
return nil, err
}
return getMachineFromComputeInstance(&instance), nil
}
func (client MachineGcpClient) UpdateMachineState(name string, state string) (bool, string, error) {
machine, err := client.GetMachine(name)
if err != nil {
return false, "", err
}
if machine == nil {
return false, fmt.Sprintf("Instance: [%s] is not found", name), nil
}
var action string
switch state {
case "Running":
action = "start"
case "Stopped":
action = "stop"
default:
return false, fmt.Sprintf("Unsupported state: %s", state), nil
}
ctx := context.Background()
endpoint := fmt.Sprintf("%s/%s/%s", client.instancesURL(), url.PathEscape(name), action)
if err := client.do(ctx, http.MethodPost, endpoint, nil); err != nil {
return false, "", err
}
return true, fmt.Sprintf("Instance: [%s]'s state has been successfully updated to: [%s]", name, state), nil
}
func (client MachineGcpClient) CreateMachine(spec *CreateMachineSpec) (*Machine, error) {
return nil, fmt.Errorf("CreateMachine not yet implemented for GCP")
}