blob: 6bff633760ad51d410bb1ea9eede07ba46c5ed73 [file] [log] [blame] [edit]
// Copyright (c) 2023 VEXXHOST, Inc.
//
// 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 main
import (
"bytes"
"context"
"encoding/json"
"fmt"
"net/url"
"os"
"path/filepath"
"strings"
"github.com/vexxhost/atmosphere/internal/portforwardutil"
"github.com/erikgeiser/promptkit/confirmation"
"github.com/nsf/jsondiff"
log "github.com/sirupsen/logrus"
"github.com/spf13/cobra"
"gopkg.in/ini.v1"
"gorm.io/driver/mysql"
"gorm.io/gorm"
v1 "k8s.io/api/core/v1"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/client-go/kubernetes"
"k8s.io/client-go/rest"
"k8s.io/client-go/tools/clientcmd"
"k8s.io/client-go/tools/portforward"
"k8s.io/client-go/util/homedir"
)
var (
kubeconfig string
portForward *portforward.PortForwarder
db *gorm.DB
instanceExtra InstanceExtra
key string
flavor *InstanceExtraFlavor
rootCmd = &cobra.Command{
Use: "extraspecfix",
Short: "Utility to fix extra_specs in the database",
PersistentPreRun: func(cmd *cobra.Command, args []string) {
instanceUUID := args[0]
key = args[1]
config, err := clientcmd.BuildConfigFromFlags("", kubeconfig)
if err != nil {
log.Fatal(err)
}
databaseConnection, err := GetDatabaseConnection(config)
if err != nil {
log.Fatal(err)
}
portForward, err = databaseConnection.PortForwarder()
if err != nil {
log.Fatal(err)
}
go func() {
err := portForward.ForwardPorts()
if err != nil {
log.Fatal(err)
}
}()
<-portForward.Ready
dsn := fmt.Sprintf(
"%s:%s@tcp(%s:%d)/%s",
databaseConnection.Username,
databaseConnection.Password,
"localhost",
3306,
databaseConnection.Database,
)
// TODO: write a custom dialer?
db, err = gorm.Open(mysql.Open(dsn), &gorm.Config{})
if err != nil {
log.Fatal(err)
}
tx := db.Where(&InstanceExtra{InstanceUUID: instanceUUID}).First(&instanceExtra)
if tx.Error != nil {
log.Fatal(err)
}
err = StrictUnmarshal([]byte(instanceExtra.Flavor), &flavor)
if err != nil {
log.Fatal(err)
}
},
PersistentPostRun: func(cmd *cobra.Command, args []string) {
serializedFlavor, err := json.Marshal(flavor)
if err != nil {
log.Fatal(err)
}
diffOpts := jsondiff.DefaultConsoleOptions()
diff, output := jsondiff.Compare(
[]byte(instanceExtra.Flavor),
[]byte(serializedFlavor),
&diffOpts,
)
if diff == jsondiff.FullMatch {
log.Info("no changes")
os.Exit(0)
}
fmt.Println(output)
input := confirmation.New("Are you ready?", confirmation.Undecided)
ready, err := input.RunPrompt()
if err != nil || !ready {
log.Info("operation cancelled")
os.Exit(0)
}
db.Model(&instanceExtra).Update("flavor", string(serializedFlavor))
portForward.Close()
},
}
setCmd = &cobra.Command{
Use: "set [instance uuid] [key] [value]",
Short: "Set an extra spec on an instance",
Args: cobra.MatchAll(cobra.ExactArgs(3), cobra.OnlyValidArgs),
Run: func(cmd *cobra.Command, args []string) {
value := args[2]
flavor.Current.Data.ExtraSpecs[key] = value
},
}
)
func init() {
homedir := homedir.HomeDir()
rootCmd.PersistentFlags().StringVar(
&kubeconfig,
"kubeconfig",
filepath.Join(homedir, ".kube", "config"),
"absolute path to the kubeconfig file",
)
rootCmd.AddCommand(setCmd)
}
type InstanceExtra struct {
ID uint `gorm:"primaryKey"`
InstanceUUID string `gorm:"column:instance_uuid"`
Flavor string `gorm:"column:flavor"`
}
type InstanceExtraFlavor struct {
Old *InstanceExtraFlavorObject `json:"old"`
Current *InstanceExtraFlavorObject `json:"cur"`
New *InstanceExtraFlavorObject `json:"new"`
}
type InstanceExtraFlavorObject struct {
Name string `json:"nova_object.name"`
Namespace string `json:"nova_object.namespace"`
Version string `json:"nova_object.version"`
Changes []string `json:"nova_object.changes"`
Data InstanceExtraFlavorObjectData `json:"nova_object.data"`
}
type InstanceExtraFlavorObjectData struct {
ID int64 `json:"id"`
FlavorID string `json:"flavorid"`
Name string `json:"name"`
Description *string `json:"description"`
VCPUs int64 `json:"vcpus"`
VCPUWeight int64 `json:"vcpu_weight"`
MemoryMB int64 `json:"memory_mb"`
RootGB int64 `json:"root_gb"`
EphemeralGB int64 `json:"ephemeral_gb"`
Swap int64 `json:"swap"`
RxTxFactor json.Number `json:"rxtx_factor"`
IsPublic bool `json:"is_public"`
Disabled bool `json:"disabled"`
ExtraSpecs map[string]string `json:"extra_specs"`
CreatedAt string `json:"created_at"`
UpdatedAt *string `json:"updated_at"`
DeletedAt *string `json:"deleted_at"`
Deleted bool `json:"deleted"`
}
func (InstanceExtra) TableName() string {
return "instance_extra"
}
func main() {
if err := rootCmd.Execute(); err != nil {
fmt.Fprintln(os.Stderr, err)
os.Exit(1)
}
}
func StrictUnmarshal(data []byte, v interface{}) error {
dec := json.NewDecoder(bytes.NewReader(data))
dec.DisallowUnknownFields()
return dec.Decode(v)
}
type DatabaseConnection struct {
config *rest.Config
clientset *kubernetes.Clientset
Username string
Password string
Service *v1.Service
Database string
}
func (db *DatabaseConnection) PortForwarder() (*portforward.PortForwarder, error) {
return portforwardutil.NewForService(db.config, db.Service, 3306)
}
func GetDatabaseConnection(config *rest.Config) (*DatabaseConnection, error) {
clientset, err := kubernetes.NewForConfig(config)
if err != nil {
return nil, err
}
secret, err := clientset.CoreV1().Secrets("openstack").Get(
context.TODO(),
"nova-etc",
metav1.GetOptions{},
)
if err != nil {
return nil, err
}
cfg, err := ini.Load(secret.Data["nova.conf"])
if err != nil {
return nil, err
}
connection := cfg.Section("database").Key("connection").String()
parsedConnection, err := url.Parse(connection)
if err != nil {
return nil, err
}
username := parsedConnection.User.Username()
password, _ := parsedConnection.User.Password()
database := strings.TrimLeft(parsedConnection.Path, "/")
hostname := parsedConnection.Hostname()
parts := strings.SplitN(hostname, ".", 3)
service := &v1.Service{
ObjectMeta: metav1.ObjectMeta{
Name: parts[0],
Namespace: parts[1],
},
}
return &DatabaseConnection{
config: config,
clientset: clientset,
Username: username,
Password: password,
Service: service,
Database: database,
}, nil
}