diff --git a/internal/controller/cluster.go b/internal/controller/cluster.go index b6387d79..dc8015b1 100644 --- a/internal/controller/cluster.go +++ b/internal/controller/cluster.go @@ -99,7 +99,7 @@ func (clctrl *ClusterController) CreateCluster() error { return fmt.Errorf(msg) } - log.Info("created %s cloud resources", clctrl.CloudProvider) + log.Infof("created %s cloud resources", clctrl.CloudProvider) telemetryShim.Transmit(clctrl.UseTelemetry, segmentClient, segment.MetricCloudTerraformApplyCompleted, "") diff --git a/internal/db/environments.go b/internal/db/environments.go new file mode 100644 index 00000000..be4f7576 --- /dev/null +++ b/internal/db/environments.go @@ -0,0 +1,104 @@ +package db + +import ( + "fmt" + + pkgtypes "github.com/kubefirst/kubefirst-api/pkg/types" + log "github.com/sirupsen/logrus" + "go.mongodb.org/mongo-driver/bson" + "go.mongodb.org/mongo-driver/bson/primitive" + "go.mongodb.org/mongo-driver/mongo" +) + +// GetEnvironments +func (mdbcl *MongoDBClient) GetEnvironments() ([]pkgtypes.Environment, error) { + // Find + var result []pkgtypes.Environment + cursor, err := mdbcl.EnvironmentsCollection.Find(mdbcl.Context, bson.D{}) + if err != nil { + return []pkgtypes.Environment{}, fmt.Errorf("error getting environments") + } + + for cursor.Next(mdbcl.Context) { + //Create a value into which the single document can be decoded + var environment pkgtypes.Environment + err := cursor.Decode(&environment) + if err != nil { + return []pkgtypes.Environment{}, err + } + result = append(result, environment) + + } + if err := cursor.Err(); err != nil { + return []pkgtypes.Environment{}, err + } + + cursor.Close(mdbcl.Context) + + return result, nil +} + +// GetEnvironment +func (mdbcl *MongoDBClient) GetEnvironment(name string) (pkgtypes.Environment, error) { + // Find + filter := bson.D{{Key: "name", Value: name }} + var result pkgtypes.Environment + err := mdbcl.EnvironmentsCollection.FindOne(mdbcl.Context, filter).Decode(&result) + if err != nil { + if err == mongo.ErrNoDocuments { + return pkgtypes.Environment{}, fmt.Errorf("environment not found") + } + return pkgtypes.Environment{}, fmt.Errorf("error getting environment %s: %s", name, err) + } + + return result, nil +} + +// InsertEnvironment +func (mdbcl *MongoDBClient) InsertEnvironment(env pkgtypes.Environment) (pkgtypes.Environment ,error) { + filter := bson.D{{ Key: "name", Value: env.Name }} + + result := pkgtypes.Environment { + ID: primitive.NewObjectID(), + Name: env.Name, + Color: env.Color, + Description: env.Description, + CreationTimestamp: env.CreationTimestamp, + } + + err := mdbcl.EnvironmentsCollection.FindOne(mdbcl.Context, filter).Decode(&result) + if err != nil { + // This error means your query did not match any documents. + if err == mongo.ErrNoDocuments { + // Create if entry does not exist + insert, err := mdbcl.EnvironmentsCollection.InsertOne(mdbcl.Context, result) + if err != nil { + return pkgtypes.Environment{}, fmt.Errorf("error inserting environment %v: %s", env.Name, err) + } + + log.Info(insert) + } + } else { + return pkgtypes.Environment{}, fmt.Errorf("environment %v already exists", env.Name) + } + return result, nil +} + +func (mdbcl *MongoDBClient) DeleteEnvironment(envName string) error { + filter := bson.D{{Key: "name", Value: envName }} + + findError := mdbcl.EnvironmentsCollection.FindOne(mdbcl.Context, filter).Err() + + if findError != nil { + return fmt.Errorf("no environment by the name %v", envName) + } + + _,err := mdbcl.EnvironmentsCollection.DeleteOne(mdbcl.Context, filter) + if err != nil { + return fmt.Errorf("error deleting environment %s: %s", envName, err) + } + + log.Infof("%v environment deleted", envName) + + return nil +} \ No newline at end of file diff --git a/internal/db/mongo.go b/internal/db/mongo.go index be49a216..59384706 100644 --- a/internal/db/mongo.go +++ b/internal/db/mongo.go @@ -23,6 +23,7 @@ type MongoDBClient struct { ClustersCollection *mongo.Collection GitopsCatalogCollection *mongo.Collection ServicesCollection *mongo.Collection + EnvironmentsCollection *mongo.Collection } var Client = Connect() @@ -65,6 +66,7 @@ func Connect() *MongoDBClient { ClustersCollection: client.Database("api").Collection("clusters"), GitopsCatalogCollection: client.Database("api").Collection("gitops-catalog"), ServicesCollection: client.Database("api").Collection("services"), + EnvironmentsCollection: client.Database("api").Collection("environments"), } return &cl diff --git a/internal/router/api/v1/environments.go b/internal/router/api/v1/environments.go new file mode 100644 index 00000000..371ac404 --- /dev/null +++ b/internal/router/api/v1/environments.go @@ -0,0 +1,78 @@ +package api + +import ( + "fmt" + "time" + + "net/http" + + "github.com/gin-gonic/gin" + "github.com/kubefirst/kubefirst-api/internal/db" + "github.com/kubefirst/kubefirst-api/internal/types" + pkgtypes "github.com/kubefirst/kubefirst-api/pkg/types" + "go.mongodb.org/mongo-driver/bson/primitive" +) + +func GetEnvironments(c *gin.Context) { + environments, err := db.Client.GetEnvironments() + + if err != nil { + c.JSON(http.StatusBadRequest, types.JSONFailureResponse{ + Message: err.Error(), + }) + return + } + + c.JSON(http.StatusOK, environments) +} + +func CreateEnvironment(c *gin.Context) { + + // Bind to variable as application/json, handle error + var environmentDefinition pkgtypes.Environment + err := c.Bind(&environmentDefinition) + if err != nil { + c.JSON(http.StatusBadRequest, types.JSONFailureResponse{ + Message: err.Error(), + }) + return + } + + environmentDefinition.CreationTimestamp = fmt.Sprintf("%v", primitive.NewDateTimeFromTime(time.Now().UTC())) + + newEnv, err := db.Client.InsertEnvironment(environmentDefinition) + + if err != nil { + c.JSON(http.StatusConflict, types.JSONFailureResponse{ + Message: err.Error(), + }) + return + } + + c.JSON(http.StatusCreated, newEnv) +} + +func DeleteEnvironment(c *gin.Context) { + envName, param := c.Params.Get("environment_name") + + if !param { + c.JSON(http.StatusBadRequest, types.JSONFailureResponse{ + Message: ":environment_name not provided", + }) + return + } + + err := db.Client.DeleteEnvironment(envName) + + if err != nil { + c.JSON(http.StatusBadRequest, types.JSONFailureResponse{ + Message: err.Error(), + }) + return + } + + c.JSON(http.StatusOK, types.JSONSuccessResponse{ + Message: fmt.Sprintf("successfully deleted %v environment", envName), + }) + +} \ No newline at end of file diff --git a/internal/router/api/v1/instanceSizes.go b/internal/router/api/v1/instanceSizes.go new file mode 100644 index 00000000..e04d2304 --- /dev/null +++ b/internal/router/api/v1/instanceSizes.go @@ -0,0 +1,198 @@ +package api + +import ( + "context" + "fmt" + "net/http" + + "github.com/gin-gonic/gin" + "github.com/kubefirst/kubefirst-api/internal/types" + "github.com/kubefirst/kubefirst-api/pkg/google" + awsinternal "github.com/kubefirst/runtime/pkg/aws" + "github.com/kubefirst/runtime/pkg/civo" + "github.com/kubefirst/runtime/pkg/digitalocean" + "github.com/kubefirst/runtime/pkg/vultr" +) + + +func ListInstanceSizesForRegion(c *gin.Context) { + dnsProvider, param := c.Params.Get("dns_provider") + + if !param { + c.JSON(http.StatusBadRequest, types.JSONFailureResponse{ + Message: ":dns_provider not provided", + }) + return + } + + var instanceSizesRequest types.InstanceSizesRequest + err := c.Bind(&instanceSizesRequest) + if err != nil { + c.JSON(http.StatusBadRequest, types.JSONFailureResponse{ + Message: err.Error(), + }) + return + } + + + switch dnsProvider { + case "civo": + if instanceSizesRequest.CivoAuth.Token == "" { + c.JSON(http.StatusBadRequest, types.JSONFailureResponse{ + Message: "missing civo auth token, try again", + }) + return + } + + var instanceSizesResponse types.CivoInstanceSizesResponse + + civoConfig := civo.CivoConfiguration{ + Client: civo.NewCivo(instanceSizesRequest.CivoAuth.Token, instanceSizesRequest.CloudRegion), + Context: context.Background(), + } + + instanceSizes, err := civoConfig.ListInstanceSizes() + if err != nil { + c.JSON(http.StatusBadRequest, types.JSONFailureResponse{ + Message: err.Error(), + }) + return + } + + instanceSizesResponse.InstanceSizes = instanceSizes + c.JSON(http.StatusOK, instanceSizesResponse) + return + + case "aws": + if instanceSizesRequest.AWSAuth.AccessKeyID == "" || + instanceSizesRequest.AWSAuth.SecretAccessKey == "" || + instanceSizesRequest.AWSAuth.SessionToken == "" { + c.JSON(http.StatusBadRequest, types.JSONFailureResponse{ + Message: "missing authentication credentials in request, please check and try again", + }) + return + } + + var instanceSizesResponse types.AwsInstanceSizesResponse + + awsConf := &awsinternal.AWSConfiguration{ + Config: awsinternal.NewAwsV3( + instanceSizesRequest.CloudRegion, + instanceSizesRequest.AWSAuth.AccessKeyID, + instanceSizesRequest.AWSAuth.SecretAccessKey, + instanceSizesRequest.AWSAuth.SessionToken, + ), + } + + if err != nil { + fmt.Println("Error describing instance offerings:", err) + return + } + + instanceSizes, err := awsConf.ListInstanceSizesForRegion() + if err != nil { + c.JSON(http.StatusBadRequest, types.JSONFailureResponse{ + Message: err.Error(), + }) + return + } + + instanceSizesResponse.InstanceSizes = instanceSizes.InstanceTypeOfferings + c.JSON(http.StatusOK, instanceSizesResponse) + return + + case "digitalocean": + if instanceSizesRequest.DigitaloceanAuth.Token == "" { + c.JSON(http.StatusBadRequest, types.JSONFailureResponse{ + Message: "missing authentication credentials in request, please check and try again", + }) + return + } + + var instanceSizesResponse types.DigitalOceanInstanceSizesResponse + + digitaloceanConf := digitalocean.DigitaloceanConfiguration{ + Client: digitalocean.NewDigitalocean(instanceSizesRequest.DigitaloceanAuth.Token), + Context: context.Background(), + } + + instances, err := digitaloceanConf.ListInstances() + if err != nil { + c.JSON(http.StatusBadRequest, types.JSONFailureResponse{ + Message: err.Error(), + }) + return + } + + + instanceSizesResponse.InstanceSizes = instances + c.JSON(http.StatusOK, instanceSizesResponse) + return + + case "vultr": + if instanceSizesRequest.VultrAuth.Token == "" { + c.JSON(http.StatusBadRequest, types.JSONFailureResponse{ + Message: "missing authentication credentials in request, please check and try again", + }) + return + } + + var instanceSizesResponse types.VultrInstanceSizesResponse + + vultrConf := vultr.VultrConfiguration{ + Client: vultr.NewVultr(instanceSizesRequest.VultrAuth.Token), + Context: context.Background(), + } + + instances, err := vultrConf.ListInstances() + if err != nil { + c.JSON(http.StatusBadRequest, types.JSONFailureResponse{ + Message: err.Error(), + }) + return + } + + instanceSizesResponse.InstanceSizes = instances + c.JSON(http.StatusOK, instanceSizesResponse) + return + + case "google": + if instanceSizesRequest.GoogleAuth.ProjectId == "" || + instanceSizesRequest.GoogleAuth.KeyFile == "" { + c.JSON(http.StatusBadRequest, types.JSONFailureResponse{ + Message: "missing authentication credentials in request, please check and try again", + }) + return + } + + googleConf := google.GoogleConfiguration{ + Context: context.Background(), + Project: instanceSizesRequest.GoogleAuth.ProjectId, + Region: instanceSizesRequest.CloudRegion, + KeyFile: instanceSizesRequest.GoogleAuth.KeyFile, + } + + instances, err := googleConf.ListInstances() + + fmt.Println("the instances =>", instances) + + if err != nil { + c.JSON(http.StatusBadRequest, types.JSONFailureResponse{ + Message: err.Error(), + }) + return + } + + c.JSON(http.StatusOK, instances); + return + + + default: + c.JSON(http.StatusBadRequest, types.JSONFailureResponse{ + Message: fmt.Sprintf("unsupported dns provider: %s", dnsProvider), + }) + return + } + +} + diff --git a/internal/router/router.go b/internal/router/router.go index 703f8151..07c8805e 100644 --- a/internal/router/router.go +++ b/internal/router/router.go @@ -72,6 +72,14 @@ func SetupRouter() *gin.Engine { // v1.GET("/domain/validate/google/:domain", middleware.ValidateAPIKey(), router.GetValidateGoogleDomain) // Regions v1.POST("/region/:cloud_provider", middleware.ValidateAPIKey(), router.PostRegions) + + // Instance Sizes + v1.POST("/instance-sizes/:dns_provider", middleware.ValidateAPIKey(), router.ListInstanceSizesForRegion) + + // Environments + v1.GET("/environment", middleware.ValidateAPIKey(), router.GetEnvironments) + v1.POST("/environment", middleware.ValidateAPIKey(), router.CreateEnvironment) + v1.DELETE("/environment/:environment_name", middleware.ValidateAPIKey(), router.DeleteEnvironment) // Utilities v1.GET("/health", router.GetHealth) diff --git a/internal/types/instanceSizes.go b/internal/types/instanceSizes.go new file mode 100644 index 00000000..5e3d7524 --- /dev/null +++ b/internal/types/instanceSizes.go @@ -0,0 +1,40 @@ +/* +Copyright (C) 2021-2023, Kubefirst + +This program is licensed under MIT. +See the LICENSE file for more details. +*/ +package types + +import ( + "github.com/aws/aws-sdk-go-v2/service/ec2/types" + "github.com/civo/civogo" + "github.com/digitalocean/godo" + pkgtypes "github.com/kubefirst/kubefirst-api/pkg/types" + vultr "github.com/vultr/govultr/v3" +) + +type InstanceSizesRequest struct { + CloudRegion string `json:"cloud_region" binding:"required"` + CivoAuth pkgtypes.CivoAuth `json:"civo_auth,omitempty"` + AWSAuth pkgtypes.AWSAuth `json:"aws_auth,omitempty"` + DigitaloceanAuth pkgtypes.DigitaloceanAuth `json:"do_auth,omitempty"` + VultrAuth pkgtypes.VultrAuth `json:"vultr_auth,omitempty"` + GoogleAuth pkgtypes.GoogleAuth `json:"google_auth,omitempty"` +} + +type CivoInstanceSizesResponse struct { + InstanceSizes []civogo.InstanceSize `json:"instance_sizes"` +} + +type AwsInstanceSizesResponse struct { + InstanceSizes []types.InstanceTypeOffering `json:"instance_sizes"` +} + +type DigitalOceanInstanceSizesResponse struct { + InstanceSizes []*godo.AppInstanceSize `json:"instance_sizes"` +} + +type VultrInstanceSizesResponse struct { + InstanceSizes []vultr.Instance `json:"instance_sizes"` +} diff --git a/internal/types/region.go b/internal/types/region.go index 883fe462..8bae0223 100644 --- a/internal/types/region.go +++ b/internal/types/region.go @@ -12,7 +12,7 @@ import ( // RegionListRequest type RegionListRequest struct { - CloudRegion string `json:"cloud_region,omitempty"` + CloudRegion string `json:"cloud_region,omitempty"` AWSAuth pkgtypes.AWSAuth `json:"aws_auth,omitempty"` CivoAuth pkgtypes.CivoAuth `json:"civo_auth,omitempty"` DigitaloceanAuth pkgtypes.DigitaloceanAuth `json:"do_auth,omitempty"` diff --git a/pkg/google/dns.go b/pkg/google/dns.go index 1332e399..df02c6e6 100644 --- a/pkg/google/dns.go +++ b/pkg/google/dns.go @@ -13,11 +13,14 @@ import ( "strings" "time" + compute "cloud.google.com/go/compute/apiv1" + "cloud.google.com/go/compute/apiv1/computepb" secretmanager "cloud.google.com/go/secretmanager/apiv1" "github.com/kubefirst/runtime/pkg/dns" "github.com/rs/zerolog/log" "golang.org/x/oauth2/google" googleDNS "google.golang.org/api/dns/v1" + "google.golang.org/api/iterator" "google.golang.org/api/option" ) @@ -144,3 +147,42 @@ func (conf *GoogleConfiguration) GetDNSDomains() ([]string, error) { return zoneNames, nil } + +func (conf *GoogleConfiguration) ListInstances() ([]string, error){ + fmt.Println("hello world") + creds, err := google.CredentialsFromJSON(conf.Context, []byte(conf.KeyFile), secretmanager.DefaultAuthScopes()...) + if err != nil { + return nil, fmt.Errorf("could not create google storage client credentials: %s", err) + } + + instancesClient, err := compute.NewInstancesRESTClient(context.Background(), option.WithCredentials(creds)); + if err != nil { + return nil, err + } + + req := &computepb.ListInstancesRequest{ + Project: conf.Project, + Zone: conf.Region, + } + + it := instancesClient.List(context.Background(), req) + + + var stuff []string + for { + instance, err := it.Next() + if err == iterator.Done { + break + } + if err != nil { + return nil, err + } + + value := fmt.Sprintf("%v : %v", instance.GetName(), instance.GetMachineType()) + fmt.Println(value) + stuff = append(stuff, value) + } + + defer instancesClient.Close() + return stuff, nil +} diff --git a/pkg/types/cluster.go b/pkg/types/cluster.go index 8d282449..2320e787 100644 --- a/pkg/types/cluster.go +++ b/pkg/types/cluster.go @@ -149,9 +149,11 @@ type ProxyImportRequest struct { } type Environment struct { - Name string `bson:"name" json:"name"` - Color string `bson:"color" json:"color"` - Description string `bson:"description,omitempty" json:"description,omitempty"` + ID primitive.ObjectID `bson:"_id" json:"_id"` + Name string `bson:"name" json:"name"` + Color string `bson:"color" json:"color"` + Description string `bson:"description,omitempty" json:"description,omitempty"` + CreationTimestamp string `bson:"creation_timestamp" json:"creation_timestamp"` } type WorkloadCluster struct { diff --git a/providers/civo/delete.go b/providers/civo/delete.go index 231ca63b..a64e2cdc 100644 --- a/providers/civo/delete.go +++ b/providers/civo/delete.go @@ -63,7 +63,7 @@ func DeleteCivoCluster(cl *pkgtypes.Cluster) error { var tfEntrypoint string if cl.GitTerraformApplyCheck { - log.Info("destroying %s resources with terraform", cl.GitProvider) + log.Infof("destroying %s resources with terraform", cl.GitProvider) switch cl.GitProvider { case "github": tfEntrypoint = config.GitopsDir + "/terraform/github" @@ -116,7 +116,7 @@ func DeleteCivoCluster(cl *pkgtypes.Cluster) error { return err } - log.Info("%s resources terraform destroyed", cl.GitProvider) + log.Infof("%s resources terraform destroyed", cl.GitProvider) err = db.Client.UpdateCluster(cl.ClusterName, "git_terraform_apply_check", false) if err != nil {