Skip to content

Commit

Permalink
feat: add harvester_bootstrap
Browse files Browse the repository at this point in the history
Signed-off-by: PoAn Yang <[email protected]>
  • Loading branch information
FrankYang0529 committed Aug 12, 2024
1 parent 45fde4b commit 951a694
Show file tree
Hide file tree
Showing 23 changed files with 655 additions and 93 deletions.
40 changes: 40 additions & 0 deletions internal/config/config.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
package config

import (
"context"
"fmt"
"strings"

metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"

"github.com/harvester/terraform-provider-harvester/pkg/client"
)

type Config struct {
Bootstrap bool
APIURL string
KubeConfig string
KubeContext string
}

func (c *Config) K8sClient() (*client.Client, error) {
return client.NewClient(c.KubeConfig, c.KubeContext)
}

func (c *Config) CheckVersion() error {
client, err := c.K8sClient()
if err != nil {
return err
}

// check harvester version from settings
serverVersion, err := client.HarvesterClient.HarvesterhciV1beta1().Settings().Get(context.Background(), "server-version", metav1.GetOptions{})
if err != nil {
return err
}
// harvester version v1.0-head, v1.0.2, v1.0.3 is not supported
if strings.HasPrefix(serverVersion.Value, "v1.0") {
return fmt.Errorf("current Harvester server version is %s, the minimum supported version is v1.1.0", serverVersion.Value)
}
return nil
}
181 changes: 181 additions & 0 deletions internal/provider/bootstrap/resource_bootstrap.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,181 @@
package bootstrap

import (
"context"
"fmt"
"log"
"net/http"
"os"

"github.com/hashicorp/terraform-plugin-sdk/v2/diag"
"github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema"

"github.com/harvester/terraform-provider-harvester/internal/config"
"github.com/harvester/terraform-provider-harvester/internal/util"
"github.com/harvester/terraform-provider-harvester/pkg/constants"
)

const (
bootstrapDefaultUser = "admin"
bootstrapDefaultTTL = "60000"
bootstrapDefaultSessionDesc = "Terraform bootstrap admin session"
)

func ResourceBootstrap() *schema.Resource {
return &schema.Resource{
CreateContext: resourceBootstrapCreate,
ReadContext: resourceBootstrapRead,
DeleteContext: resourceBootstrapDelete,
Schema: Schema(),
}
}

func resourceBootstrapCreate(ctx context.Context, d *schema.ResourceData, meta interface{}) diag.Diagnostics {
c := meta.(*config.Config)
if !c.Bootstrap {
return diag.FromErr(fmt.Errorf("harvester_bootstrap just available on bootstrap mode"))
}
initialPassword := d.Get(constants.FieldBootstrapInitialPassword).(string)
password := d.Get(constants.FieldBootstrapPassword).(string)

// login to get token
log.Printf("Doing login")
tokenID, token, err := doUserLogin(c.APIURL, bootstrapDefaultUser, initialPassword, bootstrapDefaultTTL, bootstrapDefaultSessionDesc, "", true)
if err != nil {
return diag.FromErr(err)
}
d.SetId(tokenID)

// change password
log.Printf("Doing change password")
changePasswordURL := fmt.Sprintf("%s/%s", c.APIURL, "v3/users?action=changepassword")
changePasswordData := `{"currentPassword":"` + initialPassword + `","newPassword":"` + password + `"}`
changePasswordResp, err := util.DoPost(changePasswordURL, changePasswordData, "", true, map[string]string{"Authorization": fmt.Sprintf("Bearer %s", token)})
if err != nil {
return diag.FromErr(err)
}
if changePasswordResp.StatusCode != http.StatusOK {
return diag.Errorf("failed to change password, status code %d", changePasswordResp.StatusCode)
}

// get kubeconfig
log.Printf("Doing generate kubeconfig")
genKubeConfigURL := fmt.Sprintf("%s/%s", c.APIURL, "v1/management.cattle.io.clusters/local?action=generateKubeconfig")
genKubeConfigResp, err := util.DoPost(genKubeConfigURL, "", "", true, map[string]string{"Authorization": fmt.Sprintf("Bearer %s", token)})
if err != nil {
return diag.FromErr(err)
}
if genKubeConfigResp.StatusCode != http.StatusOK {
return diag.Errorf("failed to generate kubeconfig, status code %d", genKubeConfigResp.StatusCode)
}

genKubeConfigBody, err := util.GetJSONBody(genKubeConfigResp)
if err != nil {
return diag.FromErr(err)
}
if genKubeConfigBody["config"] == nil {
return diag.FromErr(fmt.Errorf("failed to generate kubeconfig"))
}
kubeConfigContent := genKubeConfigBody["config"].(string)

// write kubeconfig
if err = os.WriteFile(d.Get(constants.FieldProviderKubeConfig).(string), []byte(kubeConfigContent), 0600); err != nil {
return diag.FromErr(err)
}

return nil
}

func resourceBootstrapRead(ctx context.Context, d *schema.ResourceData, meta interface{}) diag.Diagnostics {
c := meta.(*config.Config)
if !c.Bootstrap {
return diag.FromErr(fmt.Errorf("[ERROR] harvester_bootstrap just available on bootstrap mode"))
}

initialPassword := d.Get(constants.FieldBootstrapInitialPassword).(string)
password := d.Get(constants.FieldBootstrapPassword).(string)

// If fails, try to login with default admin user, current password and initial password if fails
loginPass := []string{
initialPassword,
password,
}

// login to get token
log.Printf("Doing login")
var (
tokenID, token string
err error
)

for _, pass := range loginPass {
tokenID, token, err = doUserLogin(c.APIURL, bootstrapDefaultUser, pass, bootstrapDefaultTTL, bootstrapDefaultSessionDesc, "", true)
if err == nil {
break
}
}
if tokenID == "" {
log.Printf("[INFO] Bootstrap is unable to login to Harvester")
d.SetId("")
return nil
}

log.Printf("Doing generate kubeconfig")
genKubeConfigURL := fmt.Sprintf("%s/%s", c.APIURL, "v1/management.cattle.io.clusters/local?action=generateKubeconfig")
genKubeConfigResp, err := util.DoPost(genKubeConfigURL, "", "", true, map[string]string{"Authorization": fmt.Sprintf("Bearer %s", token)})
if err != nil {
return diag.FromErr(err)
}
if genKubeConfigResp.StatusCode != http.StatusOK {
return diag.Errorf("failed to generate kubeconfig, status code %d", genKubeConfigResp.StatusCode)
}

genKubeConfigBody, err := util.GetJSONBody(genKubeConfigResp)
if err != nil {
return diag.FromErr(err)
}
if genKubeConfigBody["config"] == nil {
return diag.FromErr(fmt.Errorf("failed to generate kubeconfig"))
}
kubeConfigContent := genKubeConfigBody["config"].(string)

// write kubeconfig
if err = os.WriteFile(d.Get(constants.FieldProviderKubeConfig).(string), []byte(kubeConfigContent), 0600); err != nil {
return diag.FromErr(err)
}
return nil
}

func resourceBootstrapDelete(ctx context.Context, d *schema.ResourceData, meta interface{}) diag.Diagnostics {
d.SetId("")
return nil
}

func doUserLogin(url, user, pass, ttl, desc, cacert string, insecure bool) (string, string, error) {
loginURL := url + "/v3-public/localProviders/local?action=login"
loginData := `{"username": "` + user + `", "password": "` + pass + `", "ttl": ` + ttl + `, "description": "` + desc + `"}`
loginHead := map[string]string{
"Accept": "application/json",
"Content-Type": "application/json",
}

// Login with user and pass
loginResp, err := util.DoPost(loginURL, loginData, cacert, insecure, loginHead)
if err != nil {
return "", "", err
}
if loginResp.StatusCode != http.StatusCreated {
return "", "", fmt.Errorf("can't login successfully, status code %d", loginResp.StatusCode)
}

loginBody, err := util.GetJSONBody(loginResp)
if err != nil {
return "", "", err
}

if loginBody["type"].(string) != "token" || loginBody["token"] == nil {
return "", "", fmt.Errorf("doing user logging: %s %s", loginBody["type"].(string), loginBody["code"].(string))
}

return loginBody["id"].(string), loginBody["token"].(string), nil
}
39 changes: 39 additions & 0 deletions internal/provider/bootstrap/schema_bootstrap.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
package bootstrap

import (
"github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema"
"github.com/hashicorp/terraform-plugin-sdk/v2/helper/validation"

"github.com/harvester/terraform-provider-harvester/pkg/constants"
)

func Schema() map[string]*schema.Schema {
s := map[string]*schema.Schema{
constants.FieldBootstrapInitialPassword: {
Type: schema.TypeString,
Optional: true,
Sensitive: true,
ForceNew: true,
Default: "admin",
ValidateFunc: validation.NoZeroValues,
Description: "Default password in the harvester",
},
constants.FieldBootstrapPassword: {
Type: schema.TypeString,
Required: true,
Sensitive: true,
ForceNew: true,
ValidateFunc: validation.NoZeroValues,
Description: "New password for admin user",
},
constants.FieldBootstrapKubeConfig: {
Type: schema.TypeString,
Optional: true,
ForceNew: true,
Default: "~/.kube/config",
ValidateFunc: validation.NoZeroValues,
Description: "Path to store the kubeconfig file",
},
}
return s
}
7 changes: 5 additions & 2 deletions internal/provider/clusternetwork/datasource_clusternetwork.go
Original file line number Diff line number Diff line change
Expand Up @@ -8,8 +8,8 @@ import (
"github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"

"github.com/harvester/terraform-provider-harvester/internal/config"
"github.com/harvester/terraform-provider-harvester/internal/util"
"github.com/harvester/terraform-provider-harvester/pkg/client"
"github.com/harvester/terraform-provider-harvester/pkg/constants"
"github.com/harvester/terraform-provider-harvester/pkg/importer"
)
Expand All @@ -22,7 +22,10 @@ func DataSourceClusterNetwork() *schema.Resource {
}

func dataSourceClusterNetworkRead(ctx context.Context, d *schema.ResourceData, meta interface{}) diag.Diagnostics {
c := meta.(*client.Client)
c, err := meta.(*config.Config).K8sClient()
if err != nil {
return diag.FromErr(err)
}
name := d.Get(constants.FieldCommonName).(string)
clusterNetwork, err := c.HarvesterNetworkClient.NetworkV1beta1().ClusterNetworks().Get(ctx, name, metav1.GetOptions{})
if err != nil {
Expand Down
22 changes: 17 additions & 5 deletions internal/provider/clusternetwork/resource_clusternetwork.go
Original file line number Diff line number Diff line change
Expand Up @@ -11,8 +11,8 @@ import (
apierrors "k8s.io/apimachinery/pkg/api/errors"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"

"github.com/harvester/terraform-provider-harvester/internal/config"
"github.com/harvester/terraform-provider-harvester/internal/util"
"github.com/harvester/terraform-provider-harvester/pkg/client"
"github.com/harvester/terraform-provider-harvester/pkg/constants"
"github.com/harvester/terraform-provider-harvester/pkg/helper"
"github.com/harvester/terraform-provider-harvester/pkg/importer"
Expand All @@ -39,7 +39,10 @@ func ResourceClusterNetwork() *schema.Resource {
}

func resourceClusterNetworkCreate(ctx context.Context, d *schema.ResourceData, meta interface{}) diag.Diagnostics {
c := meta.(*client.Client)
c, err := meta.(*config.Config).K8sClient()
if err != nil {
return diag.FromErr(err)
}
name := d.Get(constants.FieldCommonName).(string)
if name == constants.ManagementClusterNetworkName {
return diag.FromErr(fmt.Errorf("can not create the existing %s clusternetwork, to avoid this error and continue with the plan, use `terraform import harvester_clusternetwork.%s %s` to import it first", name, name, name))
Expand All @@ -57,7 +60,10 @@ func resourceClusterNetworkCreate(ctx context.Context, d *schema.ResourceData, m
}

func resourceClusterNetworkUpdate(ctx context.Context, d *schema.ResourceData, meta interface{}) diag.Diagnostics {
c := meta.(*client.Client)
c, err := meta.(*config.Config).K8sClient()
if err != nil {
return diag.FromErr(err)
}
_, name, err := helper.IDParts(d.Id())
if err != nil {
return diag.FromErr(err)
Expand All @@ -82,7 +88,10 @@ func resourceClusterNetworkUpdate(ctx context.Context, d *schema.ResourceData, m
}

func resourceClusterNetworkRead(ctx context.Context, d *schema.ResourceData, meta interface{}) diag.Diagnostics {
c := meta.(*client.Client)
c, err := meta.(*config.Config).K8sClient()
if err != nil {
return diag.FromErr(err)
}
_, name, err := helper.IDParts(d.Id())
if err != nil {
return diag.FromErr(err)
Expand All @@ -99,7 +108,10 @@ func resourceClusterNetworkRead(ctx context.Context, d *schema.ResourceData, met
}

func resourceClusterNetworkDelete(ctx context.Context, d *schema.ResourceData, meta interface{}) diag.Diagnostics {
c := meta.(*client.Client)
c, err := meta.(*config.Config).K8sClient()
if err != nil {
return diag.FromErr(err)
}
_, name, err := helper.IDParts(d.Id())
if err != nil {
return diag.FromErr(err)
Expand Down
7 changes: 5 additions & 2 deletions internal/provider/image/datasource_image.go
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ import (
"github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"

"github.com/harvester/terraform-provider-harvester/pkg/client"
"github.com/harvester/terraform-provider-harvester/internal/config"
"github.com/harvester/terraform-provider-harvester/pkg/constants"
)

Expand All @@ -20,7 +20,10 @@ func DataSourceImage() *schema.Resource {
}

func dataSourceImageRead(ctx context.Context, d *schema.ResourceData, meta interface{}) diag.Diagnostics {
c := meta.(*client.Client)
c, err := meta.(*config.Config).K8sClient()
if err != nil {
return diag.FromErr(err)
}
namespace := d.Get(constants.FieldCommonNamespace).(string)
name := d.Get(constants.FieldCommonName).(string)
displayName := d.Get(constants.FieldImageDisplayName).(string)
Expand Down
Loading

0 comments on commit 951a694

Please sign in to comment.