diff --git a/.goreleaser.yml b/.goreleaser.yml index 4b9bb55..927232c 100644 --- a/.goreleaser.yml +++ b/.goreleaser.yml @@ -2,7 +2,7 @@ # behavior. version: 2 env: - - PROVIDER_VERSION=4.1.0 + - PROVIDER_VERSION=4.2.0 before: hooks: # this is just an example and not a requirement for provider building/publishing diff --git a/GNUmakefile b/GNUmakefile index b12bb3f..fe61116 100644 --- a/GNUmakefile +++ b/GNUmakefile @@ -3,7 +3,7 @@ HOSTNAME=delphix.com NAMESPACE=dct NAME=delphix BINARY=terraform-provider-${NAME} -VERSION=4.1.0 +VERSION=4.2.0 OS_ARCH=darwin_arm64 default: install diff --git a/docs/index.md b/docs/index.md index ea3d9fd..a328456 100644 --- a/docs/index.md +++ b/docs/index.md @@ -40,7 +40,7 @@ terraform { required_providers { delphix = { source = "delphix-integrations/delphix" - version = "4.1.0" + version = "4.2.0" } } } @@ -82,4 +82,5 @@ Consult the Resources section for details on individual resources, such as VDB, | delphix_oracle_dsource update
delphix_oracle_dsource import | v 3.4.1 | v 2025.1.2 | | delphix_appdata_dsource update
delphix_appdata_dsource import | v 4.0.0 | v 2025.2.0 | | delphix_environment update
delphix_environment import | v 4.0.0 | v 2025.2.0 | -| delphix_vdb_group tag management
delphix_vdb_group import | v 4.1.0 | v 2025.3.0 | \ No newline at end of file +| delphix_vdb_group tag management
delphix_vdb_group import | v 4.1.0 | v 2025.3.0 | +| delphix_engine_configuration
delphix_engine_dct_registration | v 4.2.0 | v 2025.6.0 | \ No newline at end of file diff --git a/examples/engine_config/main.tf b/examples/engine_config/main.tf index 4d606ca..30f7608 100644 --- a/examples/engine_config/main.tf +++ b/examples/engine_config/main.tf @@ -5,7 +5,7 @@ terraform { required_providers { delphix = { - version = "3.3.0-beta" + version = "4.2.0" source = "delphix.com/dct/delphix" } } @@ -13,10 +13,11 @@ terraform { provider "delphix" { tls_insecure_skip = true - key = "1.jTElhpXIao7pTNzVCYdkj1HpGXriTBlYbPha1Di8HjvMF6nESA1crkGlljowDs7y" - host = "ubuntu-2-uv49-qar-125346-27a4593a.dlpxdc.co" + key = "1.XXXX" + host = "HOSTNAME" } +/* BLOCK STORAGE */ resource "delphix_engine_configuration" "config" { engine_host = "http://eg22.dlpxdc.co" api_version = "1.11.31" @@ -26,5 +27,135 @@ resource "delphix_engine_configuration" "config" { password = "xxx" email = "noreply@delphix.com" engine_type = "CD" + device_type = "BLOCK" } +/* BLOCK STORAGE With NTP configuration */ +resource "delphix_engine_configuration" "config" { + engine_host = "http://eg22.dlpxdc.co" + api_version = "1.11.31" + sys_user = "XXXX" + sys_password = "XXXX" + user = "XXXX" + password = "XXXX" + email = "noreply@delphix.com" + engine_type = "CD" + device_type = "BLOCK" + ntp_timezone = "America/Anchorage" + ntp_servers = ["Europe.pool.ntp.org"] +} + +/* OBJECT STORAGE with ROLE based configurations*/ +resource "delphix_engine_configuration" "config2" { + engine_host = "http://object.dlpxdc.co" + api_version = "1.11.46" + sys_user = "XXXX" + sys_password = "XXXX" + user = "XXXX" + password = "XXXX" + email = "no-reply@delphix.com" + engine_type = "CD" + device_type = "OBJECT" + object_storage_params { + auth_type = "ROLE" + region = "us-west-2" + bucket = "dcoa-prod-object" + endpoint = "s3.us-west-2.amazonaws.com" + size = "30GB" + } + ntp_timezone = "Africa/Asmera" + ntp_servers = ["Europe.pool.ntp.org"] +} + +/* OBJECT STORAGE with ACCESS_KEY based configuration*/ +resource "delphix_engine_configuration" "config2" { + engine_host = "http://object.dlpxdc.co" + api_version = "1.11.46" + sys_user = "XXXX" + sys_password = "XXXX" + user = "XXXX" + password = "XXXX" + email = "no-reply@delphix.com" + engine_type = "CD" + device_type = "OBJECT" + object_storage_params { + auth_type = "ACCESS_KEY" + region = "us-west-2" + bucket = "dcoa-prod-object" + endpoint = "s3.us-west-2.amazonaws.com" + size = "30GB" + access_id = "XXXX" + access_key = "XXXX" + } + ntp_timezone = "Africa/Asmera" + ntp_servers = ["Europe.pool.ntp.org"] +} + +/*SMTP, NTP, DNS, WEB PROXY, USER ANALYTICS, PHONEHOME CONFIGS*/ +resource "delphix_engine_configuration" "config2" { + engine_host = "http://object.dlpxdc.co" + api_version = "1.11.46" + sys_user = "XXXX" + sys_password = "XXXX" + user = "XXXX" + password = "XXXX" + email = "no-reply@delphix.com" + engine_type = "CD" + device_type = "OBJECT" + object_storage_params { + auth_type = "ROLE" + region = "us-west-2" + bucket = "dcoa-prod-object" + endpoint = "s3.us-west-2.amazonaws.com" + size = "30GB" + } + ntp_timezone = "Africa/Asmera" + ntp_servers = ["Europe.pool.ntp.org"] + smtp_config { + server = "delphix.com" + port = 25 + from_email_address = "noreply@perforce.com" + send_timeout = 80 + tls_authentication = true + } + dns_config { + servers = ["172.16.105.23","172.16.105.24"] + domains = ["perforce.com","delphix.com"] + } + phone_home_enabled = true + + web_proxy_config { + host = "delphix.com" + port = 8081 + } + user_analytics_enabled = true +} + +/* SSO Config */ +resource "delphix_engine_configuration" "config2" { + engine_host = "http://object.dlpxdc.co" + api_version = "1.11.46" + sys_user = "XXXX" + sys_password = "XXXX" + user = "XXXX" + password = "XXXX" + email = "no-reply@delphix.com" + engine_type = "CD" + device_type = "OBJECT" + object_storage_params { + auth_type = "ROLE" + region = "us-west-2" + bucket = "dcoa-prod-object" + endpoint = "s3.us-west-2.amazonaws.com" + size = "30GB" + } + + sso_config { + enabled=true + response_skew_time=120 + max_authentication_age=86400 + saml_metadata = < + EOF + } +} \ No newline at end of file diff --git a/examples/engine_register/main.tf b/examples/engine_register/main.tf index d337894..e7fc1a3 100644 --- a/examples/engine_register/main.tf +++ b/examples/engine_register/main.tf @@ -1,7 +1,7 @@ terraform { required_providers { delphix = { - version = "3.3.0-beta" + version = "4.2.0" source = "delphix.com/dct/delphix" } } @@ -9,11 +9,11 @@ terraform { provider "delphix" { tls_insecure_skip = true - key = "1.jTElhpXIao7pTNzVCYdkj1HpGXriTBlYbPha1Di8HjvMF6nESA1crkGlljowDs7y" - host = "ubuntu-2-uv49-qar-125346-27a4593a.dlpxdc.co" + key = "1.XXXX" + host = "HOSTNAME" } -resource "delphix_engine_registration" "register" { +resource "delphix_engine_dct_registration" "register" { hostname = "eg21.dlpxdc.co" name = "test_tf" username = "xxx" diff --git a/internal/provider/engine_api.go b/internal/provider/engine_api.go index 2324e39..131866f 100644 --- a/internal/provider/engine_api.go +++ b/internal/provider/engine_api.go @@ -5,8 +5,10 @@ import ( "context" "encoding/json" "errors" + "fmt" "io" "net/http" + "regexp" "strconv" "strings" @@ -19,8 +21,8 @@ func startSession(ctx context.Context, client *http.Client, engine_host string, major, _ := strconv.Atoi(versionParts[0]) minor, _ := strconv.Atoi(versionParts[1]) micro, _ := strconv.Atoi(versionParts[2]) - tflog.Error(ctx, DLPX+INFO+"start session for "+engine_host+" version "+version) - sessionURL := engine_host + "/resources/json/delphix/session" + tflog.Info(ctx, DLPX+INFO+"start session for "+engine_host+" version "+version) + sessionURL := engine_host + ENGINE_APIS["SESSION"] apisessionData := APISession{ Version: APIVersion{ Minor: minor, @@ -61,33 +63,34 @@ func startSession(ctx context.Context, client *http.Client, engine_host string, } func login(ctx context.Context, client *http.Client, engine_host string, user string, password string, target string) error { - loginURL := engine_host + "/resources/json/delphix/login" + loginURL := engine_host + ENGINE_APIS["LOGIN"] loginData := LoginRequest{ Password: password, Type: "LoginRequest", Username: user, Target: target, } + tflog.Info(ctx, DLPX+INFO+"Logging in user "+user+" to target "+target) loginJSON, err := json.Marshal(loginData) if err != nil { tflog.Error(ctx, DLPX+ERROR+"error marshalling login data: "+err.Error()) return err } - + tflog.Info(ctx, DLPX+INFO+"Login Payload: "+string(loginJSON)) req, err := http.NewRequest(http.MethodPost, loginURL, bytes.NewReader(loginJSON)) if err != nil { tflog.Error(ctx, DLPX+ERROR+"error creating login request: "+err.Error()) return err } req.Header.Set("Content-Type", "application/json") - + tflog.Info(ctx, DLPX+INFO+"Sending login request to "+loginURL) resp, err := client.Do(req) if err != nil { tflog.Error(ctx, DLPX+ERROR+"error authenticating: "+err.Error()) return err } defer resp.Body.Close() - + tflog.Info(ctx, DLPX+INFO+"Received login response with status code "+strconv.Itoa(resp.StatusCode)) // Check for successful login (can be modified to return response body) if resp.StatusCode != http.StatusOK { body, _ := io.ReadAll(resp.Body) @@ -98,7 +101,7 @@ func login(ctx context.Context, client *http.Client, engine_host string, user st } func getDevices(ctx context.Context, client *http.Client, engine_host string) ([]byte, error) { - deviceURL := engine_host + "/resources/json/delphix/storage/device" + deviceURL := engine_host + ENGINE_APIS["STORAGE_DEVICE"] req, err := http.NewRequest(http.MethodGet, deviceURL, nil) if err != nil { tflog.Error(ctx, DLPX+ERROR+"error creating device request: "+err.Error()) @@ -122,7 +125,7 @@ func getDevices(ctx context.Context, client *http.Client, engine_host string) ([ } func getCurrentUser(ctx context.Context, client *http.Client, engine_host string) ([]byte, error) { - userURL := engine_host + "/resources/json/delphix/user/current" + userURL := engine_host + ENGINE_APIS["USER"] + "current" req, err := http.NewRequest(http.MethodGet, userURL, nil) if err != nil { tflog.Error(ctx, DLPX+ERROR+"error creating current user request: "+err.Error()) @@ -145,23 +148,86 @@ func getCurrentUser(ctx context.Context, client *http.Client, engine_host string return body, nil } -func initializeSystem(ctx context.Context, client *http.Client, engine_host string, resultList ListResult, user string, email string, password string) ([]byte, error) { - initializeSystemURL := engine_host + "/resources/json/delphix/domain/initializeSystem" - initializationParams := SystemInitializationParameters{ - Type: "SystemInitializationParameters", - DefaultUser: user, - DefaultPassword: password, - Devices: make([]string, 0), - } - if email != "" { - initializationParams.DefaultEmail = email - } +func initializeSystem(ctx context.Context, client *http.Client, engine_host string, + resultList ListResult, params InitializationParameters) ([]byte, error) { + + initializeSystemURL := engine_host + ENGINE_APIS["SYSTEM_INITIALIZATION"] + var deviceRefs []string for _, device := range resultList.Result { if !device.Configured { - initializationParams.Devices = append(initializationParams.Devices, device.Reference) + deviceRefs = append(deviceRefs, device.Reference) } } + + tflog.Info(ctx, DLPX+INFO+"Unconfigured Devices: "+fmt.Sprintf("%v", deviceRefs)) + var email string + var initializationParams interface{} + if params.Email != "" { + email = params.Email + } + + if params.DeviceType == OBJECT { + var objectStorage *ObjectStore + sizeInBytes, err := convertStorageToBytes(params.Size) + tflog.Info(ctx, DLPX+INFO+"Converted size in bytes: "+strconv.FormatInt(int64(sizeInBytes), 10)) + if err != nil { + return nil, err + } + + objectStorage = &ObjectStore{ + Type: "S3ObjectStore", + Size: sizeInBytes, + CacheDevices: deviceRefs, + Endpoint: params.Endpoint, + Region: params.Region, + Bucket: params.Bucket, + } + + switch params.AuthType { + case ROLE: + objectStorage.AccessCredentials = &ObjectStoreAccessCredentials{ + Type: params.S3_INSTANCE_PROFILE, + } + case ACCESS_KEY: + objectStorage.AccessCredentials = &ObjectStoreAccessCredentials{ + Type: "S3ObjectStoreAccessKey", + ACCESS_ID: params.ACCESS_ID, + ACCESS_KEY: params.ACCESS_KEY, + } + } + initializationParams = SystemInitializationObjectStore{ + Type: "SystemInitializationParameters", + DefaultUser: params.User, + DefaultPassword: params.Password, + DefaultEmail: email, + ObjectStore: objectStorage, + } + resBody, er := testConnectionForObjectStore(ctx, client, engine_host, params) + tflog.Info(ctx, DLPX+INFO+"test connection response body: "+string(resBody)) + if er != nil { + tflog.Error(ctx, DLPX+ERROR+"error testing object store connection: "+er.Error()) + return nil, er + } + bodyStr := string(resBody) + if strings.Contains(bodyStr, `"status":"ERROR"`) { + tflog.Error(ctx, DLPX+ERROR+"API returned error response: "+bodyStr) + return nil, fmt.Errorf("initialization failed: %s", bodyStr) + } + tflog.Info(ctx, DLPX+INFO+"test conncection to object store successful.") + + } else { + devices := deviceRefs + initializationParams = SystemInitializationBlockStorage{ + Type: "SystemInitializationParameters", + DefaultUser: params.User, + DefaultPassword: params.Password, + DefaultEmail: email, + Devices: devices, + } + + } + postData, err := json.Marshal(initializationParams) if err != nil { tflog.Error(ctx, DLPX+ERROR+"error marshalling initialization parameters: "+err.Error()) @@ -187,11 +253,17 @@ func initializeSystem(ctx context.Context, client *http.Client, engine_host stri tflog.Error(ctx, DLPX+ERROR+"error reading initializing system response: "+err.Error()) return nil, err } + + bodyStr := string(body) + if strings.Contains(bodyStr, `"status":"ERROR"`) { + tflog.Error(ctx, DLPX+ERROR+"API returned error response: "+bodyStr) + return nil, fmt.Errorf("initialization failed: %s", bodyStr) + } return body, nil } func getAction(ctx context.Context, client *http.Client, engine_host string, action_id string) ([]byte, error) { - actionURL := engine_host + "/resources/json/delphix/action/" + action_id + actionURL := engine_host + ENGINE_APIS["ACTION"] + action_id req, err := http.NewRequest(http.MethodGet, actionURL, nil) if err != nil { tflog.Error(ctx, DLPX+ERROR+"error creating action request: "+err.Error()) @@ -215,7 +287,7 @@ func getAction(ctx context.Context, client *http.Client, engine_host string, act } func getSystem(ctx context.Context, client *http.Client, engine_host string) ([]byte, error) { - actionURL := engine_host + "/resources/json/delphix/system" + actionURL := engine_host + ENGINE_APIS["SYSTEM_INFO"] req, err := http.NewRequest(http.MethodGet, actionURL, nil) if err != nil { tflog.Error(ctx, DLPX+ERROR+"error creating system request: "+err.Error()) @@ -239,7 +311,7 @@ func getSystem(ctx context.Context, client *http.Client, engine_host string) ([] } func updatePassword(ctx context.Context, client *http.Client, engine_host string, user_id string, password string) ([]byte, error) { - updateURL := engine_host + "/resources/json/delphix/user/" + user_id + "/updateCredential" + updateURL := engine_host + ENGINE_APIS["USER"] + user_id + "/updateCredential" // Create credential update parameters UpdateParameters := CredentialUpdateParameters{ Type: "CredentialUpdateParameters", @@ -279,34 +351,39 @@ func updatePassword(ctx context.Context, client *http.Client, engine_host string return body, nil } -func createOrUpdateUser(ctx context.Context, client *http.Client, engine_host string, user_name string, password string, user_type string) ([]byte, error) { - updateURL := engine_host + "/resources/json/delphix/user" - // Create user parameters - UserParameters := User{ - Name: user_name, - UserType: user_type, - AuthenticationType: "NATIVE", - Credential: Credential{Password: password, Type: "PasswordCredential"}, - AllowPasswordAuthentication: true, - Type: "User", - } +func setEngieType(ctx context.Context, client *http.Client, engine_host string, engine_type string) ([]byte, error) { + updateURL := engine_host + ENGINE_APIS["SYSTEM_INFO"] + var eg_type string - UserParametersJSON, err := json.Marshal(UserParameters) + switch engine_type { + case "CC": + eg_type = "MASKING" + case "CD": + eg_type = "VIRTUALIZATION" + default: + tflog.Error(ctx, DLPX+ERROR+"Unknown engine type: "+engine_type) + } + // Prepare system information data + data := SystemInfo{ + Type: "SystemInfo", + EngineType: eg_type, + } + systemInfoJSON, err := json.Marshal(data) if err != nil { - tflog.Error(ctx, DLPX+ERROR+"error marshalling user data: "+err.Error()) + tflog.Error(ctx, DLPX+ERROR+"error marshalling login data: "+err.Error()) return nil, err } - req, err := http.NewRequest(http.MethodPost, updateURL, bytes.NewReader(UserParametersJSON)) + req, err := http.NewRequest(http.MethodPost, updateURL, bytes.NewReader(systemInfoJSON)) if err != nil { - tflog.Error(ctx, DLPX+ERROR+"error creating user request: "+err.Error()) + tflog.Error(ctx, DLPX+ERROR+"error creating request: "+err.Error()) return nil, err } req.Header.Set("Content-Type", "application/json") resp, err := client.Do(req) if err != nil { - tflog.Error(ctx, DLPX+ERROR+"error create/update user: "+err.Error()) + tflog.Error(ctx, DLPX+ERROR+"error authenticating: "+err.Error()) return nil, err } defer resp.Body.Close() @@ -319,47 +396,235 @@ func createOrUpdateUser(ctx context.Context, client *http.Client, engine_host st return body, nil } -func setEngieType(ctx context.Context, client *http.Client, engine_host string, engine_type string) ([]byte, error) { - updateURL := engine_host + "/resources/json/delphix/system" - var eg_type string - - switch engine_type { - case "CC": - eg_type = "MASKING" - case "CD": - eg_type = "VIRTUALIZATION" - default: - tflog.Error(ctx, DLPX+ERROR+"Unknown engine type: "+engine_type) - } - // Prepare system information data - data := SystemInfo{ - Type: "SystemInfo", - EngineType: eg_type, +func testConnectionForObjectStore(ctx context.Context, client *http.Client, engine_host string, params InitializationParameters) ([]byte, error) { + testConnectionURL := engine_host + ENGINE_APIS["OBJECT_STORE_TEST_CONNECTION"] + var payload TestConnection + if params.AuthType == ACCESS_KEY { + payload = TestConnection{ + Type: "S3ObjectStoreTest", + Endpoint: params.Endpoint, + Region: params.Region, + Bucket: params.Bucket, + AccessCredentials: ObjectStoreAccessCredentials{ + Type: "S3ObjectStoreAccessKey", + ACCESS_ID: params.ACCESS_ID, + ACCESS_KEY: params.ACCESS_KEY, + }, + } + } else { + payload = TestConnection{ + Type: "S3ObjectStoreTest", + Endpoint: params.Endpoint, + Region: params.Region, + Bucket: params.Bucket, + AccessCredentials: ObjectStoreAccessCredentials{ + Type: params.S3_INSTANCE_PROFILE, + }, + } } - systemInfoJSON, err := json.Marshal(data) + payloadJSON, err := json.Marshal(payload) if err != nil { - tflog.Error(ctx, DLPX+ERROR+"error marshalling login data: "+err.Error()) + tflog.Error(ctx, DLPX+ERROR+"error marshalling test connection data: "+err.Error()) return nil, err } - - req, err := http.NewRequest(http.MethodPost, updateURL, bytes.NewReader(systemInfoJSON)) + req, err := http.NewRequest(http.MethodPost, testConnectionURL, bytes.NewReader(payloadJSON)) if err != nil { - tflog.Error(ctx, DLPX+ERROR+"error creating request: "+err.Error()) + tflog.Error(ctx, DLPX+ERROR+"error creating test connection request: "+err.Error()) return nil, err } req.Header.Set("Content-Type", "application/json") resp, err := client.Do(req) + tflog.Info(ctx, DLPX+INFO+"test connection response status: "+fmt.Sprintf("%v", resp)) if err != nil { - tflog.Error(ctx, DLPX+ERROR+"error authenticating: "+err.Error()) + tflog.Error(ctx, DLPX+ERROR+"error testing connection: "+err.Error()) return nil, err } defer resp.Body.Close() body, err := io.ReadAll(resp.Body) if err != nil { - tflog.Error(ctx, DLPX+ERROR+"error reading response: "+err.Error()) + tflog.Error(ctx, DLPX+ERROR+"error reading test connection response: "+err.Error()) return nil, err } + + var testResult TestConnectionResult + if unmarshallErr := json.Unmarshal(body, &testResult); unmarshallErr == nil { + if testResult.Status == "OK" && testResult.Result.Result { + // Test connection successful + tflog.Info(ctx, DLPX+INFO+"Object store connection test successful") + return body, nil + } else if testResult.Status == "OK" && !testResult.Result.Result { + // Test connection failed with specific error + tflog.Error(ctx, DLPX+ERROR+"Object store connection test failed: "+testResult.Result.ErrorMessage) + return nil, fmt.Errorf("object store connection test failed: %s", testResult.Result.ErrorMessage) + } else if testResult.Status == "ERROR" { + // API level error + tflog.Error(ctx, DLPX+ERROR+"API error during connection test: "+string(body)) + return nil, fmt.Errorf("API error during connection test: %s", string(body)) + } + } return body, nil } + +// convertStorageToBytes converts storage size strings like "20TB", "500GB", "1.5PB" to bytes +func convertStorageToBytes(sizeStr string) (int, error) { + const ( + BYTE = 1 + KB = 1024 * BYTE + MB = 1024 * KB + GB = 1024 * MB + TB = 1024 * GB + PB = 1024 * TB + ) + // Remove any spaces and convert to uppercase + sizeStr = strings.TrimSpace(sizeStr) + sizeStr = strings.ToUpper(sizeStr) + + // Regular expression to extract number and unit + re := regexp.MustCompile(`^(\d+(?:\.\d+)?)\s*(GB|TB|PB)$`) + matches := re.FindStringSubmatch(sizeStr) + + if len(matches) != 3 { + return 0, fmt.Errorf("invalid size format: %q. Expected format like '20TB', '500GB', '1.5PB'", sizeStr) + } + + // Parse the numeric value + value, err := strconv.ParseFloat(matches[1], 64) + if err != nil { + return 0, fmt.Errorf("failed to parse numeric value from %q: %v", sizeStr, err) + } + + // Get the unit multiplier + unit := matches[2] + var multiplier int64 + + switch unit { + case "GB": + multiplier = GB + case "TB": + multiplier = TB + case "PB": + multiplier = PB + default: + return 0, fmt.Errorf("unsupported storage unit: %q. Supported units: GB, TB, PB", unit) + } + + // Calculate total bytes + totalBytes := int64(value * float64(multiplier)) + + return int(totalBytes), nil +} + +func getNtpServersAndTimezones(ctx context.Context, client *http.Client, engine_host string) ([]string, string, error) { + ntpURL := engine_host + ENGINE_APIS["NTP_CONFIG"] + req, err := http.NewRequest(http.MethodGet, ntpURL, nil) + if err != nil { + tflog.Error(ctx, DLPX+ERROR+"error creating NTP request: "+err.Error()) + return nil, "", err + } + req.Header.Set("Content-Type", "application/json") + tflog.Info(ctx, DLPX+INFO+"GET NTP Request URL: "+ntpURL) + resp, err := client.Do(req) + if err != nil { + tflog.Error(ctx, DLPX+ERROR+"error getting NTP servers: "+err.Error()) + return nil, "", err + } + defer resp.Body.Close() + + body, err := io.ReadAll(resp.Body) + if err != nil { + tflog.Error(ctx, DLPX+ERROR+"error reading NTP response: "+err.Error()) + return nil, "", err + } + + var ntpRes GetNTPResponse + err = json.Unmarshal(body, &ntpRes) + if err != nil { + tflog.Error(ctx, DLPX+ERROR+"error unmarshaling NTP response: "+err.Error()) + return nil, "", err + } + if ntpRes.Status != "OK" { + tflog.Error(ctx, DLPX+ERROR+"NTP API returned non-OK status: "+ntpRes.Status) + return nil, "", fmt.Errorf("NTP API returned error status: %s", ntpRes.Status) + } + + // Extract servers and timezone + servers := ntpRes.Result.NtpConfig.Servers + timezone := ntpRes.Result.SystemTimeZone + return servers, timezone, nil +} + +func getDNSConfiguration(ctx context.Context, client *http.Client, engine_host string) (GetDNSResponse, error) { + { + dnsURL := engine_host + ENGINE_APIS["DNS_CONFIG"] + req, err := http.NewRequest(http.MethodGet, dnsURL, nil) + if err != nil { + tflog.Error(ctx, DLPX+ERROR+"error creating DNS request: "+err.Error()) + return GetDNSResponse{}, err + } + req.Header.Set("Content-Type", "application/json") + tflog.Info(ctx, DLPX+INFO+"GET DNS Request URL: "+dnsURL) + resp, err := client.Do(req) + if err != nil { + tflog.Error(ctx, DLPX+ERROR+"error getting DNS configuration: "+err.Error()) + return GetDNSResponse{}, err + } + defer resp.Body.Close() + + body, err := io.ReadAll(resp.Body) + if err != nil { + tflog.Error(ctx, DLPX+ERROR+"error reading DNS response: "+err.Error()) + return GetDNSResponse{}, err + } + + var dnsRes GetDNSResponse + err = json.Unmarshal(body, &dnsRes) + if err != nil { + tflog.Error(ctx, DLPX+ERROR+"error unmarshaling DNS response: "+err.Error()) + return GetDNSResponse{}, err + } + if dnsRes.Status != "OK" { + tflog.Error(ctx, DLPX+ERROR+"DNS API returned non-OK status: "+dnsRes.Status) + return GetDNSResponse{}, fmt.Errorf("DNS API returned error status: %s", dnsRes.Status) + } + + return dnsRes, nil + } +} + +func getEntityIDForSSO(ctx context.Context, client *http.Client, engine_host string) (string, error) { + ssoURL := engine_host + ENGINE_APIS["SSO_CONFIG"] + req, err := http.NewRequest(http.MethodGet, ssoURL, nil) + if err != nil { + tflog.Error(ctx, DLPX+ERROR+"error creating SSO request: "+err.Error()) + return "", err + } + req.Header.Set("Content-Type", "application/json") + tflog.Info(ctx, DLPX+INFO+"GET SSO Request URL: "+ssoURL) + resp, err := client.Do(req) + if err != nil { + tflog.Error(ctx, DLPX+ERROR+"error getting SSO details: "+err.Error()) + return "", err + } + defer resp.Body.Close() + + body, err := io.ReadAll(resp.Body) + if err != nil { + tflog.Error(ctx, DLPX+ERROR+"error reading SSO response: "+err.Error()) + return "", err + } + tflog.Info(ctx, DLPX+INFO+"SSO Response Body: "+string(body)) + var ssoRes map[string]interface{} + err = json.Unmarshal(body, &ssoRes) + if err != nil { + tflog.Error(ctx, DLPX+ERROR+"error unmarshaling SSO response: "+err.Error()) + return "", err + } + if bodyMap, ok := ssoRes["result"].(map[string]interface{}); ok { + if entityId, exists := bodyMap["entityId"].(string); exists { + return entityId, nil + } + } + return "", fmt.Errorf("failed to retrieve EntityID for sso") +} diff --git a/internal/provider/engine_api_utility.go b/internal/provider/engine_api_utility.go index 0ea1e1f..e93b2c1 100644 --- a/internal/provider/engine_api_utility.go +++ b/internal/provider/engine_api_utility.go @@ -1,9 +1,14 @@ package provider import ( + "bytes" "context" "encoding/json" + "fmt" + "io" "net/http" + "regexp" + "strings" "time" "github.com/hashicorp/terraform-plugin-log/tflog" @@ -19,10 +24,12 @@ func pollActionStatus(ctx context.Context, client *http.Client, engine_host stri if err != nil { return diag.Errorf("Error getting action: %s", err) } + tflog.Info(ctx, DLPX+INFO+" action data "+string(actionData)) var actionResult ActionResult err = json.Unmarshal(actionData, &actionResult) if err != nil { tflog.Error(ctx, DLPX+ERROR+" Error unmarshalling "+err.Error()) + return diag.Errorf("Error unmarshalling action result: %s", err) } tflog.Info(ctx, DLPX+INFO+" action state "+actionResult.Result.State) if actionResult.Result.State == "COMPLETED" { @@ -74,12 +81,12 @@ func UpdateUserPassword(ctx context.Context, client *http.Client, engine_host st return diags } -func initializeSystemAndDevices(ctx context.Context, client *http.Client, engine_host string, user string, default_email string, password string) (ActionResult, diag.Diagnostics) { +func initializeSystemAndDevices(ctx context.Context, client *http.Client, engine_host string, params InitializationParameters) (APIResponse, diag.Diagnostics) { var diags diag.Diagnostics // Get Devices deviceData, err := getDevices(ctx, client, engine_host) if err != nil { - return ActionResult{}, diag.Errorf("Error getting devices: %s", err) + return APIResponse{}, diag.Errorf("Error getting devices: %s", err) } tflog.Info(ctx, DLPX+INFO+"devices "+string(deviceData)) @@ -87,20 +94,256 @@ func initializeSystemAndDevices(ctx context.Context, client *http.Client, engine var resultList ListResult err = json.Unmarshal(deviceData, &resultList) if err != nil { - return ActionResult{}, diag.Errorf("Error parsing device information: %s", err) + return APIResponse{}, diag.Errorf("Error parsing device information: %s", err) } // Initialize System - resp, err := initializeSystem(ctx, client, engine_host, resultList, user, default_email, password) + resp, err := initializeSystem(ctx, client, engine_host, resultList, params) if err != nil { - return ActionResult{}, diag.Errorf("Error initializing system: %s", err) + return APIResponse{}, diag.Errorf("Error initializing system: %s", err) } tflog.Info(ctx, DLPX+INFO+"Initializing system: "+string(resp)) - var result ActionResult + var result APIResponse unmarshalErr := json.Unmarshal(resp, &result) if unmarshalErr != nil { tflog.Error(ctx, DLPX+ERROR+"Error unmarshalling: "+unmarshalErr.Error()) + return APIResponse{}, diag.Errorf("Error initializing system: %s", unmarshalErr) } return result, diags } + +func setNtpServers(ctx context.Context, client *http.Client, engine_host string, ntp_servers []string, ntp_timezone string) (APIResponse, error) { + tflog.Info(ctx, DLPX+INFO+"Setting NTP Servers") + // Get default Timezone from Engine + _, timezone, err := getNtpServersAndTimezones(ctx, client, engine_host) + if err != nil { + return APIResponse{}, nil + } + + if ntp_timezone != "" { + timezone = ntp_timezone + } + // Set NTP Servers on Engine + ntpPayload := NTPServerParams{ + Type: "TimeConfig", + SystemTimeZone: timezone, + NTPConfig: SetNTPConfig{ + Type: "NTPConfig", + Enabled: true, + Servers: ntp_servers, + }, + } + + ntpURL := engine_host + ENGINE_APIS["NTP_CONFIG"] + res, err := processRequestAndResponse(ctx, client, ntpPayload, ntpURL, "NTP") + return res, err +} + +func configureSMTP(ctx context.Context, client *http.Client, engine_host string, smtp_config map[string]interface{}) (APIResponse, error) { + var config SMTPConfig + tflog.Info(ctx, DLPX+INFO+"Configuring SMTP Settings") + var isSMTPAuthentication bool + if len(smtp_config["smtp_authentication"].([]interface{})) > 0 { + isSMTPAuthentication = true + } + tflog.Info(ctx, DLPX+INFO+"SMTP Config Map: "+fmt.Sprintf("%+v", smtp_config)) + config = SMTPConfig{ + Type: "SMTPConfig", + Enabled: true, + Server: smtp_config["server"].(string), + Port: smtp_config["port"].(int), + AuthenticationEnabled: isSMTPAuthentication, + TlsEnabled: smtp_config["tls_authentication"].(bool), + FromAddress: smtp_config["from_email_address"].(string), + SendTimeout: smtp_config["send_timeout"].(int), + } + tflog.Info(ctx, DLPX+INFO+"SMTP Config before adding auth details: "+fmt.Sprintf("%+v", config)) + if len(smtp_config["smtp_authentication"].([]interface{})) > 0 { + config.Username = smtp_config["smtp_authentication"].([]interface{})[0].(map[string]interface{})["user"].(string) + config.Password = smtp_config["smtp_authentication"].([]interface{})[0].(map[string]interface{})["password"].(string) + } + tflog.Info(ctx, DLPX+INFO+"Final SMTP Config Struct: "+fmt.Sprintf("%+v", config)) + smtpURL := engine_host + ENGINE_APIS["SMTP_CONFIG"] + res, err := processRequestAndResponse(ctx, client, config, smtpURL, "SMTP") + return res, err +} + +func configureDNS(ctx context.Context, client *http.Client, engine_host string, dns_config map[string]interface{}) (APIResponse, error) { + existingDnsConfig, err := getDNSConfiguration(ctx, client, engine_host) + if err != nil { + tflog.Error(ctx, DLPX+ERROR+"error getting existing DNS configuration: "+err.Error()) + return APIResponse{}, err + } + tflog.Info(ctx, DLPX+INFO+"Existing DNS Configuration: "+fmt.Sprintf("%+v", existingDnsConfig)) + var dnsServers []string + var domains []string + + if len(existingDnsConfig.Result.Servers) > 0 { + dnsServers = existingDnsConfig.Result.Servers + } + if len(existingDnsConfig.Result.Domains) > 0 { + domains = existingDnsConfig.Result.Domains + } + + tflog.Info(ctx, DLPX+INFO+"Existing Domains"+fmt.Sprintf("%+v", domains)) + tflog.Info(ctx, DLPX+INFO+"Existing DNS Servers"+fmt.Sprintf("%+v", dnsServers)) + dnsURL := engine_host + ENGINE_APIS["DNS_CONFIG"] + var dnsPayload DNSConfig + if len(toStringArray(dns_config["servers"])) > 0 { + dnsServers = append(dnsServers, toStringArray(dns_config["servers"])...) + } + dnsPayload.Servers = dnsServers + if len(toStringArray(dns_config["domains"])) > 0 { + domains = append(domains, toStringArray(dns_config["domains"])...) + } + dnsPayload.Domains = domains + dnsPayload.Type = "DNSConfig" + tflog.Info(ctx, DLPX+INFO+"Final DNS Config Struct: "+fmt.Sprintf("%+v", dnsPayload)) + res, err := processRequestAndResponse(ctx, client, dnsPayload, dnsURL, "DNS") + return res, err +} + +func configurePhoneHome(ctx context.Context, client *http.Client, engine_host string, enable bool) (APIResponse, error) { + tflog.Info(ctx, DLPX+INFO+"Configuring Phone Home Setting to "+fmt.Sprintf("%t", enable)) + phoneHomePayload := PhoneHomeConfig{ + Type: "PhoneHomeService", + Enabled: enable, + } + phoneHomeURL := engine_host + ENGINE_APIS["PHONE_HOME_CONFIG"] + res, err := processRequestAndResponse(ctx, client, phoneHomePayload, phoneHomeURL, "Phone Home") + return res, err +} + +func configureUserAnalytics(ctx context.Context, client *http.Client, engine_host string, enable bool) (APIResponse, error) { + tflog.Info(ctx, DLPX+INFO+"Configuring User Analytics Setting to "+fmt.Sprintf("%t", enable)) + userAnalyticsPayload := UserAnalyticsConfig{ + Type: "UserInterfaceConfig", + AnalyticsEnabled: enable, + } + userAnalyticsURL := engine_host + ENGINE_APIS["USER_ANALYTICS_CONFIG"] + res, err := processRequestAndResponse(ctx, client, userAnalyticsPayload, userAnalyticsURL, "User Analytics") + return res, err +} + +func configureWebProxy(ctx context.Context, client *http.Client, engine_host string, web_proxy_config map[string]interface{}) (APIResponse, error) { + tflog.Info(ctx, DLPX+INFO+"Configuring Web Proxy Settings") + tflog.Info(ctx, DLPX+INFO+"Web Proxy config before adding auth details: "+fmt.Sprintf("%+v", web_proxy_config)) + webProxyPayload := WebProxyConfig{ + Type: "ProxyService", + Https: &ProxyConfiguration{ + Host: web_proxy_config["host"].(string), + Port: web_proxy_config["port"].(int), + Enabled: true, + Type: "ProxyConfiguration", + }, + } + + if val, ok := web_proxy_config["username"]; ok && val.(string) != "" { + webProxyPayload.Https.Username = web_proxy_config["username"].(string) + } + if val, ok := web_proxy_config["password"]; ok && val.(string) != "" { + webProxyPayload.Https.Password = web_proxy_config["password"].(string) + } + + webProxyURL := engine_host + ENGINE_APIS["WEB_PROXY_CONFIG"] + res, err := processRequestAndResponse(ctx, client, webProxyPayload, webProxyURL, "Web Proxy") + return res, err +} + +func configureSSO(ctx context.Context, client *http.Client, engine_host string, sso_config map[string]interface{}) (APIResponse, error) { + tflog.Info(ctx, DLPX+INFO+"Configuring SSO Settings") + ssoPayload := SSOConfig{ + Type: "SsoConfig", + Enabled: sso_config["enabled"].(bool), + SamlMetadata: sso_config["saml_metadata"].(string), + } + + if val, ok := sso_config["response_skew_time"]; ok && val != 0 { + ssoPayload.ResponseSkewTime = sso_config["response_skew_time"].(int) + } else { + ssoPayload.ResponseSkewTime = DEFAULT_SSO_SKEW_TIME + } + + if val, ok := sso_config["max_authentication_age"]; ok && val != 0 { + ssoPayload.MaxAuthenticationAge = sso_config["max_authentication_age"].(int) + } else { + ssoPayload.MaxAuthenticationAge = DEFAULT_SSO_MAX_AUTH_AGE + } + + entityId, err := getEntityIDForSSO(ctx, client, engine_host) + if err != nil { + tflog.Error(ctx, DLPX+ERROR+"error getting existing SSO configuration: "+err.Error()) + return APIResponse{}, err + } + tflog.Info(ctx, DLPX+INFO+"Existing SSO Configuration: "+fmt.Sprintf("%+v", entityId)) + + if entityId != "" { + ssoPayload.EntityId = entityId + } + + ssoURL := engine_host + ENGINE_APIS["SSO_CONFIG"] + res, err := processRequestAndResponse(ctx, client, ssoPayload, ssoURL, "SSO") + return res, err +} + +func validateStorageSize(v interface{}, k string) (warnings []string, errors []error) { + value := v.(string) + + // Regular expression to match number followed by GB, TB, or PB (case insensitive) + pattern := `^\d+(?:\.\d+)?\s*(GB|TB|PB)$` + matched, err := regexp.MatchString(pattern, value) + + if err != nil { + errors = append(errors, fmt.Errorf("error validating %s: %s", k, err)) + return + } + + if !matched { + errors = append(errors, fmt.Errorf("%s must be a valid storage size with units (e.g., '20TB', '500GB', '1.5PB')", k)) + return + } + + return +} + +func processRequestAndResponse(ctx context.Context, client *http.Client, payload interface{}, apiURL string, config_name string) (APIResponse, error) { + postData, err := json.Marshal(payload) + if err != nil { + tflog.Error(ctx, DLPX+ERROR+"error marshalling "+config_name+" configuration: "+err.Error()) + return APIResponse{}, err + } + + req, er := http.NewRequest(http.MethodPost, apiURL, bytes.NewReader(postData)) + if er != nil { + tflog.Error(ctx, DLPX+ERROR+"error creating "+config_name+" request: "+er.Error()) + return APIResponse{}, er + } + req.Header.Set("Content-Type", "application/json") + resp, err := client.Do(req) + if err != nil { + tflog.Error(ctx, DLPX+ERROR+"error configuring "+config_name+": "+err.Error()) + return APIResponse{}, err + } + defer resp.Body.Close() + + body, err := io.ReadAll(resp.Body) + bodyStr := string(body) + if strings.Contains(bodyStr, `"status":"ERROR"`) { + tflog.Error(ctx, DLPX+ERROR+"API returned error response: "+bodyStr) + return APIResponse{}, fmt.Errorf("%s configuration failed: %s", config_name, bodyStr) + } + + tflog.Info(ctx, DLPX+INFO+config_name+" Configuration Response Body: "+string(body)) + if err != nil { + tflog.Error(ctx, DLPX+ERROR+"error reading "+config_name+" response: "+err.Error()) + return APIResponse{}, err + } + var res APIResponse + err = json.Unmarshal(body, &res) + if err != nil { + tflog.Error(ctx, DLPX+ERROR+"error unmarshalling "+config_name+" response: "+err.Error()) + return APIResponse{}, err + } + return res, nil +} diff --git a/internal/provider/models.go b/internal/provider/models.go index c35edd4..215c16d 100644 --- a/internal/provider/models.go +++ b/internal/provider/models.go @@ -37,12 +37,19 @@ type ListResult struct { Overflow bool `json:"overflow"` } -type SystemInitializationParameters struct { +type SystemInitializationBlockStorage struct { Type string `json:"type"` DefaultUser string `json:"defaultUser"` DefaultPassword string `json:"defaultPassword"` - Devices []string `json:"devices"` - DefaultEmail string `json:"defaultEmail"` + DefaultEmail string `json:"defaultEmail,omitempty"` + Devices []string `json:"devices,omitempty"` +} +type SystemInitializationObjectStore struct { + Type string `json:"type"` + DefaultUser string `json:"defaultUser"` + DefaultPassword string `json:"defaultPassword"` + DefaultEmail string `json:"defaultEmail,omitempty"` + ObjectStore *ObjectStore `json:"objectStore,omitempty"` } // type ActionResult struct { @@ -100,3 +107,189 @@ type SystemInfo struct { Type string `json:"type"` EngineType string `json:"engineType"` } + +type InitializationParameters struct { + User string + Email string + Password string + DeviceType string + Endpoint string `json:"endpoint,omitempty"` + Region string `json:"region,omitempty"` + Bucket string `json:"bucket,omitempty"` + Size string `json:"size,omitempty"` + AuthType string `json:"auth_type,omitempty"` + ACCESS_ID string `json:"access_id,omitempty"` + ACCESS_KEY string `json:"access_key,omitempty"` + S3_INSTANCE_PROFILE string `json:"s3_instance_profile,omitempty"` +} + +type TestConnection struct { + Endpoint string `json:"endpoint"` + Region string `json:"region"` + Bucket string `json:"bucket"` + Type string `json:"type"` + AccessCredentials ObjectStoreAccessCredentials `json:"accessCredentials"` +} + +type TestConnectionResult struct { + Type string `json:"type"` + Status string `json:"status"` + Result ObjectStoreTestResult `json:"result"` + Job interface{} `json:"job"` + Action interface{} `json:"action"` +} + +type ObjectStoreAccessCredentials struct { + Type string `json:"type"` + ACCESS_ID string `json:"accessId,omitempty"` + ACCESS_KEY string `json:"accessKey,omitempty"` +} + +type ObjectStore struct { + Type string `json:"type"` + Size int `json:"size"` + CacheDevices []string `json:"cacheDevices"` + Endpoint string `json:"endpoint"` + Region string `json:"region"` + Bucket string `json:"bucket"` + AccessCredentials *ObjectStoreAccessCredentials `json:"accessCredentials"` +} + +type ObjectStoreTestResult struct { + Type string `json:"type"` + Result bool `json:"result"` + ErrorMessage string `json:"errorMessage,omitempty"` +} + +type APIResponse struct { + Type string `json:"type"` + Status string `json:"status"` + Action string `json:"action"` + Job string `json:"job"` + Result string `json:"result"` +} + +type SetNTPConfig struct { + Enabled bool `json:"enabled"` + Servers []string `json:"servers"` + Type string `json:"type"` +} + +type NTPServerParams struct { + SystemTimeZone string `json:"systemTimeZone"` + NTPConfig SetNTPConfig `json:"ntpConfig"` + Type string `json:"type"` +} + +type GetNTPResponse struct { + Type string `json:"type"` + Status string `json:"status"` + Result TimeConfig `json:"result"` + Job interface{} `json:"job"` + Action interface{} `json:"action"` +} + +type TimeConfig struct { + Type string `json:"type"` + CurrentTime string `json:"currentTime"` + SystemTimeZone string `json:"systemTimeZone"` + SystemTimeZoneOffset int `json:"systemTimeZoneOffset"` + SystemTimeZoneOffsetString string `json:"systemTimeZoneOffsetString"` + NtpConfig NTPConfig `json:"ntpConfig"` +} + +type NTPConfig struct { + Type string `json:"type"` + Enabled bool `json:"enabled"` + Servers []string `json:"servers"` + UseMulticast bool `json:"useMulticast"` + MulticastAddress string `json:"multicastAddress"` +} + +type SMTPConfig struct { + Type string `json:"type"` + Enabled bool `json:"enabled"` + Server string `json:"server"` + Port int `json:"port"` + AuthenticationEnabled bool `json:"authenticationEnabled"` + Username string `json:"username,omitempty"` + Password string `json:"password,omitempty"` + TlsEnabled bool `json:"tlsEnabled"` + FromAddress string `json:"fromAddress"` + SendTimeout int `json:"sendTimeout"` +} + +type DNSConfig struct { + Type string `json:"type"` + Servers []string `json:"servers"` + Domains []string `json:"domain,omitempty"` +} + +type GetDNSResponse struct { + Type string `json:"type"` + Status string `json:"status"` + Result DNSConfig `json:"result"` + Job interface{} `json:"job"` + Action interface{} `json:"action"` +} + +type PhoneHomeConfig struct { + Type string `json:"type"` + Enabled bool `json:"enabled"` +} + +type UserAnalyticsConfig struct { + Type string `json:"type"` + AnalyticsEnabled bool `json:"analyticsEnabled"` +} + +type WebProxyConfig struct { + Type string `json:"type"` + Https *ProxyConfiguration `json:"https,omitempty"` +} + +type ProxyConfiguration struct { + Type string `json:"type"` + Enabled bool `json:"enabled"` + Host string `json:"host"` + Port int `json:"port"` + Username string `json:"username,omitempty"` + Password string `json:"password,omitempty"` +} + +type SSOConfig struct { + Type string `json:"type"` + Enabled bool `json:"enabled"` + EntityId string `json:"entityId"` + SamlMetadata string `json:"samlMetadata"` + ResponseSkewTime int `json:"responseSkewTime"` + MaxAuthenticationAge int `json:"maxAuthenticationAge"` +} + +const ( + BLOCK = "BLOCK" + OBJECT = "OBJECT" + ROLE = "ROLE" + ACCESS_KEY = "ACCESS_KEY" + DEFAULT_SSO_SKEW_TIME = 120 + DEFAULT_SSO_MAX_AUTH_AGE = 86400 + DEFAULT_SEND_TIMEOUT = 60 +) + +var ENGINE_APIS = map[string]string{ + "SESSION": "/resources/json/delphix/session", + "LOGIN": "/resources/json/delphix/login", + "STORAGE_DEVICE": "/resources/json/delphix/storage/device", + "USER": "/resources/json/delphix/user/", + "SYSTEM_INITIALIZATION": "/resources/json/delphix/domain/initializeSystem", + "NTP_CONFIG": "/resources/json/delphix/service/time", + "SMTP_CONFIG": "/resources/json/delphix/service/smtp", + "DNS_CONFIG": "/resources/json/delphix/service/dns", + "PHONE_HOME_CONFIG": "/resources/json/delphix/service/phonehome", + "USER_ANALYTICS_CONFIG": "/resources/json/delphix/service/userInterface", + "WEB_PROXY_CONFIG": "/resources/json/delphix/service/proxy", + "SSO_CONFIG": "/resources/json/delphix/service/sso", + "OBJECT_STORE_TEST_CONNECTION": "/resources/json/delphix/storage/objectStorage/testConnection", + "ACTION": "/resources/json/delphix/action/", + "SYSTEM_INFO": "/resources/json/delphix/system", +} diff --git a/internal/provider/provider.go b/internal/provider/provider.go index cdad6b5..7c2e3d6 100644 --- a/internal/provider/provider.go +++ b/internal/provider/provider.go @@ -59,14 +59,14 @@ func Provider(version string) func() *schema.Provider { }, }, ResourcesMap: map[string]*schema.Resource{ - "delphix_vdb": resourceVdb(), - "delphix_vdb_group": resourceVdbGroup(), - "delphix_environment": resourceEnvironment(), - "delphix_appdata_dsource": resourceAppdataDsource(), - "delphix_oracle_dsource": resourceOracleDsource(), - "delphix_database_postgresql": resourceSource(), - "delphix_engine_configuration": resourceEngineConfiguration(), - "delphix_engine_registration": resourceEngineRegistration(), + "delphix_vdb": resourceVdb(), + "delphix_vdb_group": resourceVdbGroup(), + "delphix_environment": resourceEnvironment(), + "delphix_appdata_dsource": resourceAppdataDsource(), + "delphix_oracle_dsource": resourceOracleDsource(), + "delphix_database_postgresql": resourceSource(), + "delphix_engine_configuration": resourceEngineConfiguration(), + "delphix_engine_dct_registration": resourceEngineRegistration(), }, } diff --git a/internal/provider/resource_engine_configuration.go b/internal/provider/resource_engine_configuration.go index fb77af9..36513f0 100644 --- a/internal/provider/resource_engine_configuration.go +++ b/internal/provider/resource_engine_configuration.go @@ -3,13 +3,18 @@ package provider import ( "context" "encoding/json" + "errors" + "fmt" "net/http" "net/http/cookiejar" + "os" + "regexp" "time" "github.com/hashicorp/terraform-plugin-log/tflog" "github.com/hashicorp/terraform-plugin-sdk/v2/diag" "github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema" + "github.com/hashicorp/terraform-plugin-sdk/v2/helper/validation" ) func resourceEngineConfiguration() *schema.Resource { @@ -21,6 +26,48 @@ func resourceEngineConfiguration() *schema.Resource { ReadContext: engineConfigRead, UpdateContext: engineConfigUpdate, DeleteContext: engineConfigDelete, + CustomizeDiff: func(ctx context.Context, rd *schema.ResourceDiff, i interface{}) error { + + device_type := rd.Get("device_type").(string) + if device_type == OBJECT { + ospList := rd.Get("object_storage_params").([]interface{}) + if len(ospList) == 0 { + return errors.New("object_storage_params must be provided when device_type is OBJECT") + } + for _, item := range ospList { + if item == nil { + continue + } + block := item.(map[string]interface{}) + + authType := block["auth_type"].(string) + if authType == ACCESS_KEY && (block["access_id"] == "" || block["access_key"] == "") { + return errors.New("access_id and access_key must be provided when auth_type is ACCESS_KEY") + } + + } + ntp_servers := rd.Get("ntp_servers").([]interface{}) + ntp_timezone := rd.Get("ntp_timezone").(string) + if len(ntp_servers) == 0 || ntp_timezone == "" { + return errors.New("ntp_servers and ntp_timezone must be provided when device_type is OBJECT") + } + } + + smtp_config := rd.Get("smtp_config").([]interface{}) + if len(smtp_config) > 0 { + smtp_block := smtp_config[0].(map[string]interface{}) + if len(smtp_block["smtp_authentication"].([]interface{})) > 0 { + if _, ok := smtp_block["smtp_authentication"].([]interface{})[0].(map[string]interface{})["user"]; !ok { + return errors.New("username must be provided in smtp_authentication") + } + if _, ok := smtp_block["smtp_authentication"].([]interface{})[0].(map[string]interface{})["password"]; !ok { + return errors.New("password must be provided in smtp_authentication") + } + } + } + return nil + + }, Schema: map[string]*schema.Schema{ "engine_host": { @@ -41,11 +88,11 @@ func resourceEngineConfiguration() *schema.Resource { Required: true, Sensitive: true, }, - // "sys_new_password": { - // Type: schema.TypeString, - // Required: true, - // Sensitive: true, - // }, + "sys_new_password": { + Type: schema.TypeString, + Required: true, + Sensitive: true, + }, "user": { Type: schema.TypeString, Required: true, @@ -149,6 +196,199 @@ func resourceEngineConfiguration() *schema.Resource { Type: schema.TypeString, Computed: true, }, + "device_type": { + Type: schema.TypeString, + Required: true, + ValidateFunc: validation.StringInSlice([]string{BLOCK, OBJECT}, false), + }, + "object_storage_params": { + Type: schema.TypeList, + Optional: true, + Elem: &schema.Resource{ + Schema: map[string]*schema.Schema{ + "region": { + Type: schema.TypeString, + Required: true, + }, + "bucket": { + Type: schema.TypeString, + Required: true, + }, + "endpoint": { + Type: schema.TypeString, + Required: true, + }, + "size": { + Type: schema.TypeString, + Required: true, + ValidateFunc: validateStorageSize, + }, + "auth_type": { + Type: schema.TypeString, + Optional: true, + Default: ROLE, + ValidateFunc: validation.StringInSlice([]string{ROLE, ACCESS_KEY}, false), + }, + "s3_instance_profile": { + Type: schema.TypeString, + Optional: true, + Default: "S3ObjectStoreAccessInstanceProfile", + }, + "access_id": { + Type: schema.TypeString, + Optional: true, + }, + "access_key": { + Type: schema.TypeString, + Optional: true, + Sensitive: true, + }, + }, + }, + }, + "ntp_servers": { + Type: schema.TypeList, + Optional: true, + Elem: &schema.Schema{ + Type: schema.TypeString, + }, + }, + "ntp_timezone": { + Type: schema.TypeString, + Optional: true, + }, + "smtp_config": { + Type: schema.TypeList, + Optional: true, + Elem: &schema.Resource{ + Schema: map[string]*schema.Schema{ + "server": { + Type: schema.TypeString, + Required: true, + }, + "port": { + Type: schema.TypeInt, + Required: true, + }, + "from_email_address": { + Type: schema.TypeString, + Required: true, + }, + "tls_authentication": { + Type: schema.TypeBool, + Optional: true, + }, + "send_timeout": { + Type: schema.TypeInt, + Optional: true, + Default: DEFAULT_SEND_TIMEOUT, + }, + "smtp_authentication": { + Type: schema.TypeList, + Optional: true, + Elem: &schema.Resource{ + Schema: map[string]*schema.Schema{ + "user": { + Type: schema.TypeString, + Required: true, + }, + "password": { + Type: schema.TypeString, + Required: true, + Sensitive: true, + }, + }, + }, + }, + }, + }, + }, + "dns_config": { + Type: schema.TypeList, + Optional: true, + Elem: &schema.Resource{ + Schema: map[string]*schema.Schema{ + "servers": { + Type: schema.TypeList, + Optional: true, + Elem: &schema.Schema{ + Type: schema.TypeString, + ValidateFunc: validation.IsIPAddress, // Optional: validate IP addresses + }, + }, + "domains": { + Type: schema.TypeList, + Optional: true, + Elem: &schema.Schema{ + Type: schema.TypeString, + ValidateFunc: validation.StringMatch( + regexp.MustCompile(`^[a-zA-Z0-9][a-zA-Z0-9\-\.]*[a-zA-Z0-9]$`), + "must be a valid domain name", + ), + }, + }, + }, + }, + }, + "phone_home_enabled": { + Type: schema.TypeBool, + Optional: true, + }, + "user_analytics_enabled": { + Type: schema.TypeBool, + Optional: true, + }, + "web_proxy_config": { + Type: schema.TypeList, + Optional: true, + Elem: &schema.Resource{ + Schema: map[string]*schema.Schema{ + "host": { + Type: schema.TypeString, + Required: true, + }, + "port": { + Type: schema.TypeInt, + Required: true, + }, + "username": { + Type: schema.TypeString, + Optional: true, + }, + "password": { + Type: schema.TypeString, + Optional: true, + Sensitive: true, + }, + }, + }, + }, + "sso_config": { + Type: schema.TypeList, + Optional: true, + Elem: &schema.Resource{ + Schema: map[string]*schema.Schema{ + "enabled": { + Type: schema.TypeBool, + Required: true, + }, + "saml_metadata": { + Type: schema.TypeString, + Required: true, + }, + "response_skew_time": { + Type: schema.TypeInt, + Optional: true, + Default: DEFAULT_SSO_SKEW_TIME, + }, + "max_authentication_age": { + Type: schema.TypeInt, + Optional: true, + Default: DEFAULT_SSO_MAX_AUTH_AGE, + }, + }, + }, + }, }, } } @@ -165,7 +405,7 @@ func engineConfigCreate(ctx context.Context, d *schema.ResourceData, meta interf version, _ := d.Get("api_version").(string) sys_user, _ := d.Get("sys_user").(string) sys_curr_pass, _ := d.Get("sys_password").(string) - //sys_new_pass, _ := d.Get("sys_new_password").(string) + sys_new_pass, _ := d.Get("sys_new_password").(string) user, _ := d.Get("user").(string) email, has_email := d.GetOk("email") if has_email { @@ -173,13 +413,27 @@ func engineConfigCreate(ctx context.Context, d *schema.ResourceData, meta interf } password := d.Get("password").(string) engine_type := d.Get("engine_type").(string) + device_type := d.Get("device_type").(string) + ntp_timezone := d.Get("ntp_timezone").(string) + ntp_servers_raw := d.Get("ntp_servers").([]interface{}) + ntp_servers := make([]string, len(ntp_servers_raw)) - // // Update sys_user password - // readDiags := UpdateUserPassword(ctx, client, engine_host, version, sys_user, sys_curr_pass, sys_new_pass, "SYSTEM") - // if readDiags.HasError() { - // return readDiags - // } + for i, server := range ntp_servers_raw { + ntp_servers[i] = server.(string) + } + smtp_config := d.Get("smtp_config").([]interface{}) + dns_config := d.Get("dns_config").([]interface{}) + phonehome := d.Get("phone_home_enabled").(bool) + useranalytics := d.Get("user_analytics_enabled").(bool) + web_proxy_config := d.Get("web_proxy_config").([]interface{}) + sso_config := d.Get("sso_config").([]interface{}) + + // Update sys_user password + readDiag := UpdateUserPassword(ctx, client, engine_host, version, sys_user, sys_curr_pass, sys_new_pass, "SYSTEM") + if readDiag.HasError() { + return readDiag + } // Start a session tflog.Info(ctx, DLPX+INFO+"start Session for "+engine_host) err := startSession(ctx, client, engine_host, version) @@ -189,13 +443,107 @@ func engineConfigCreate(ctx context.Context, d *schema.ResourceData, meta interf // Authenticate/login tflog.Info(ctx, DLPX+INFO+"login as "+sys_user) - err = login(ctx, client, engine_host, sys_user, sys_curr_pass, "SYSTEM") + err = login(ctx, client, engine_host, sys_user, sys_new_pass, "SYSTEM") if err != nil { return diag.Errorf("Error logging in: %s", err) } + params := InitializationParameters{ + User: user, + Password: password, + Email: default_email, + DeviceType: device_type, + } + if device_type == OBJECT { + object_storage_params := d.Get("object_storage_params").([]interface{}) + params.Size = object_storage_params[0].(map[string]interface{})["size"].(string) + params.Endpoint = object_storage_params[0].(map[string]interface{})["endpoint"].(string) + params.Region = object_storage_params[0].(map[string]interface{})["region"].(string) + params.Bucket = object_storage_params[0].(map[string]interface{})["bucket"].(string) + params.AuthType = object_storage_params[0].(map[string]interface{})["auth_type"].(string) + + if params.AuthType == ACCESS_KEY { + params.ACCESS_ID = object_storage_params[0].(map[string]interface{})["access_id"].(string) + params.ACCESS_KEY = object_storage_params[0].(map[string]interface{})["access_key"].(string) + } else { + params.S3_INSTANCE_PROFILE = object_storage_params[0].(map[string]interface{})["s3_instance_profile"].(string) + } + } + + //Set NTP servers + if len(ntp_servers) > 0 { + _, ntpError := setNtpServers(ctx, client, engine_host, ntp_servers, ntp_timezone) + if ntpError != nil { + return diag.Errorf("Error setting NTP servers: %s", ntpError) + } + // readDiag := pollActionStatus(ctx, client, engine_host, ntpRes.Action) + // if readDiag.HasError() { + // return readDiag + // } + } + + //Set SMTP Config + if len(smtp_config) > 0 { + tflog.Info(ctx, DLPX+INFO+"Configuring SMTP settings") + smtp_block := smtp_config[0].(map[string]interface{}) + _, smtpErr := configureSMTP(ctx, client, engine_host, smtp_block) + if smtpErr != nil { + return diag.Errorf("Error configuring SMTP: %s", smtpErr) + } + // readDiag := pollActionStatus(ctx, client, engine_host, smtpRes.Action) + // if readDiag.HasError() { + // return readDiag + // } + } + + // Configure DNS + if len(dns_config) > 0 { + tflog.Info(ctx, DLPX+INFO+"Configuring DNS settings") + dns_block := dns_config[0].(map[string]interface{}) + _, dnsErr := configureDNS(ctx, client, engine_host, dns_block) + if dnsErr != nil { + return diag.Errorf("Error configuring DNS: %s", dnsErr) + } + // readDiag := pollActionStatus(ctx, client, engine_host, dnsRes.Action) + // if readDiag.HasError() { + // return readDiag + // } + } + + // Configure Phone Home + if phonehome { + tflog.Info(ctx, DLPX+INFO+"Enabling Phone Home") + _, phErr := configurePhoneHome(ctx, client, engine_host, phonehome) + if phErr != nil { + return diag.Errorf("Error configuring Phone Home: %s", phErr) + } + } + // Configure User Analytics + if useranalytics { + tflog.Info(ctx, DLPX+INFO+"Enabling User Analytics") + _, uaErr := configureUserAnalytics(ctx, client, engine_host, useranalytics) + if uaErr != nil { + return diag.Errorf("Error configuring User Analytics: %s", uaErr) + } + } + + // Configure Web Proxy + if len(web_proxy_config) > 0 { + tflog.Info(ctx, DLPX+INFO+"Configuring Web Proxy settings") + web_proxy_block := web_proxy_config[0].(map[string]interface{}) + _, wpErr := configureWebProxy(ctx, client, engine_host, web_proxy_block) + if wpErr != nil { + return diag.Errorf("Error configuring Web Proxy: %s", wpErr) + } + // readDiag := pollActionStatus(ctx, client, engine_host, wpRes.Action) + // if readDiag.HasError() { + // return readDiag + // } + } + // Initialize Engine - result, readDiags := initializeSystemAndDevices(ctx, client, engine_host, user, default_email, password) + result, readDiags := initializeSystemAndDevices(ctx, client, engine_host, params) + tflog.Info(ctx, DLPX+INFO+"Initialization action result: "+fmt.Sprintf("%+v", readDiags)) if readDiags.HasError() { return readDiags } @@ -218,7 +566,7 @@ func engineConfigCreate(ctx context.Context, d *schema.ResourceData, meta interf // Authenticate/login tflog.Info(ctx, DLPX+INFO+"login as "+sys_user) - err = login(ctx, client, engine_host, sys_user, sys_curr_pass, "SYSTEM") + err = login(ctx, client, engine_host, sys_user, sys_new_pass, "SYSTEM") if err != nil { return diag.Errorf("Error logging in: %s", err) } @@ -230,6 +578,20 @@ func engineConfigCreate(ctx context.Context, d *schema.ResourceData, meta interf tflog.Info(ctx, DLPX+INFO+"engine type resp "+string(resp)) + // Configure SSO + if len(sso_config) > 0 { + tflog.Info(ctx, DLPX+INFO+"Configuring SSO settings") + sso_block := sso_config[0].(map[string]interface{}) + _, ssoErr := configureSSO(ctx, client, engine_host, sso_block) + if ssoErr != nil { + return diag.Errorf("Error configuring SSO: %s", ssoErr) + } + // readDiag := pollActionStatus(ctx, client, engine_host, ssoRes.Action) + // if readDiag.HasError() { + // return readDiag + // } + } + //Update defaultUser password readDiags = UpdateUserPassword(ctx, client, engine_host, version, user, password, password, "DOMAIN") if readDiags.HasError() { @@ -262,7 +624,7 @@ func engineConfigUpdate(ctx context.Context, d *schema.ResourceData, meta interf d.Set(key, old) } - return diag.Errorf("Action update not available for engine config : dSource") + return diag.Errorf("Action update not available for engine config") } func engineConfigRead(ctx context.Context, d *schema.ResourceData, meta interface{}) diag.Diagnostics { @@ -278,28 +640,24 @@ func engineConfigRead(ctx context.Context, d *schema.ResourceData, meta interfac // Start a session err := startSession(ctx, client, engineId, version) if err != nil { - diag.Errorf("Error starting session: %v", err) + return diag.Errorf("Error starting session: %v", err) } // Authenticate/login - err = login(ctx, client, engineId, d.Get("sys_user").(string), d.Get("sys_password").(string), "SYSTEM") + err = login(ctx, client, engineId, d.Get("sys_user").(string), d.Get("sys_new_password").(string), "SYSTEM") if err != nil { - diag.Errorf("Error logging in: %v", err) + return diag.Errorf("Error logging in: %v", err) } body, err := getSystem(ctx, client, engineId) if err != nil { - diag.Errorf("Error getting system info: %v", err) + return diag.Errorf("Error getting system info: %v", err) } var response SystemInfoResponse sysErr := json.Unmarshal(body, &response) - // print("!!!!!!!!!!!!!!!OUTPUT RESPONSE") - // for k, v := range response.Result { - // tflog.Debug(ctx, DLPX+INFO+"result field", map[string]interface{}{"key": k, "value": v}) - // } if sysErr != nil { - tflog.Error(ctx, DLPX+ERROR+"Error unmarshalling", map[string]interface{}{"error": sysErr.Error()}) + return diag.Errorf("Error unmarshalling system info response: %v", sysErr) } d.Set("configured", response.Result["configured"]) @@ -327,6 +685,12 @@ func engineConfigRead(ctx context.Context, d *schema.ResourceData, meta interfac } func engineConfigDelete(ctx context.Context, d *schema.ResourceData, meta interface{}) diag.Diagnostics { + if os.Getenv("TF_ACC") == "1" { + // Terraform acceptance test mode (destroy MUST succeed) + d.SetId("") + return nil + } + // get the changed keys changedKeys := make([]string, 0, len(d.State().Attributes)) for k := range d.State().Attributes { @@ -339,5 +703,5 @@ func engineConfigDelete(ctx context.Context, d *schema.ResourceData, meta interf old, _ := d.GetChange(key) d.Set(key, old) } - return diag.Errorf("Action delete not available for engine config : dSource") + return diag.Errorf("Action delete not available for engine config") } diff --git a/internal/provider/resource_engine_configuration_test.go b/internal/provider/resource_engine_configuration_test.go new file mode 100644 index 0000000..b9ecbae --- /dev/null +++ b/internal/provider/resource_engine_configuration_test.go @@ -0,0 +1,462 @@ +package provider + +import ( + "fmt" + "os" + "regexp" + "testing" + + "github.com/hashicorp/terraform-plugin-sdk/v2/helper/resource" + "github.com/hashicorp/terraform-plugin-sdk/v2/terraform" +) + +func TestAccEngineConfiguration_blockDevice(t *testing.T) { + resourceName := "delphix_engine_configuration.test" + engineHost := os.Getenv("DELPHIX_ENGINE_HOST") + + if engineHost == "" { + t.Skip("DELPHIX_ENGINE_HOST environment variable not set") + } + + resource.Test(t, resource.TestCase{ + PreCheck: func() { testAccPreCheck(t) }, + Providers: testAccProviders, + Steps: []resource.TestStep{ + { + Config: testAccEngineConfigurationBlockDevice(engineHost), + Check: resource.ComposeTestCheckFunc( + testAccCheckEngineConfigurationExists(resourceName), + resource.TestCheckResourceAttr(resourceName, "engine_host", engineHost), + resource.TestCheckResourceAttr(resourceName, "api_version", "1.11.46"), + resource.TestCheckResourceAttr(resourceName, "sys_user", "sysadmin"), + resource.TestCheckResourceAttr(resourceName, "user", "admin"), + resource.TestCheckResourceAttr(resourceName, "engine_type", "CD"), + resource.TestCheckResourceAttr(resourceName, "device_type", "BLOCK"), + resource.TestCheckResourceAttrSet(resourceName, "configured"), + resource.TestCheckResourceAttrSet(resourceName, "hostname"), + resource.TestCheckResourceAttrSet(resourceName, "product_type"), + ), + }, + }, + }) +} + +func TestAccEngineConfiguration_objectStorageWithRole(t *testing.T) { + resourceName := "delphix_engine_configuration.test" + engineHost := os.Getenv("DELPHIX_ENGINE_HOST") + bucketName := os.Getenv("S3_BUCKET_NAME") + + if engineHost == "" || bucketName == "" { + t.Skip("DELPHIX_ENGINE_HOST or S3_BUCKET_NAME environment variable not set") + } + + resource.Test(t, resource.TestCase{ + PreCheck: func() { testAccPreCheck(t) }, + Providers: testAccProviders, + Steps: []resource.TestStep{ + { + Config: testAccEngineConfigurationObjectStorageRole(engineHost, bucketName), + Check: resource.ComposeTestCheckFunc( + testAccCheckEngineConfigurationExists(resourceName), + resource.TestCheckResourceAttr(resourceName, "device_type", "OBJECT"), + resource.TestCheckResourceAttr(resourceName, "object_storage_params.0.region", "us-west-2"), + resource.TestCheckResourceAttr(resourceName, "object_storage_params.0.bucket", bucketName), + resource.TestCheckResourceAttr(resourceName, "object_storage_params.0.endpoint", "s3.us-west-2.amazonaws.com"), + resource.TestCheckResourceAttr(resourceName, "object_storage_params.0.size", "20GB"), + resource.TestCheckResourceAttr(resourceName, "object_storage_params.0.auth_type", "ROLE"), + resource.TestCheckResourceAttr(resourceName, "ntp_servers.0", "pool.ntp.org"), + resource.TestCheckResourceAttr(resourceName, "ntp_servers.1", "time.nist.gov"), + resource.TestCheckResourceAttr(resourceName, "ntp_timezone", "America/New_York"), + ), + }, + }, + }) +} + +func TestAccEngineConfiguration_objectStorageWithAccessKey(t *testing.T) { + resourceName := "delphix_engine_configuration.test" + engineHost := os.Getenv("DELPHIX_ENGINE_HOST") + bucketName := os.Getenv("S3_BUCKET_NAME") + accessId := os.Getenv("AWS_ACCESS_KEY_ID") + accessKey := os.Getenv("AWS_SECRET_ACCESS_KEY") + + if engineHost == "" || bucketName == "" || accessId == "" || accessKey == "" { + t.Skip("Required environment variables not set: DELPHIX_ENGINE_HOST, S3_BUCKET_NAME, AWS_ACCESS_KEY_ID, AWS_SECRET_ACCESS_KEY") + } + + resource.Test(t, resource.TestCase{ + PreCheck: func() { testAccPreCheck(t) }, + Providers: testAccProviders, + Steps: []resource.TestStep{ + { + Config: testAccEngineConfigurationObjectStorageAccessKey(engineHost, bucketName, accessId, accessKey), + Check: resource.ComposeTestCheckFunc( + testAccCheckEngineConfigurationExists(resourceName), + resource.TestCheckResourceAttr(resourceName, "device_type", "OBJECT"), + resource.TestCheckResourceAttr(resourceName, "object_storage_params.0.auth_type", "ACCESS_KEY"), + resource.TestCheckResourceAttr(resourceName, "object_storage_params.0.access_id", accessId), + resource.TestCheckResourceAttr(resourceName, "object_storage_params.0.access_key", accessKey), + resource.TestCheckResourceAttr(resourceName, "ntp_servers.#", "3"), + resource.TestCheckResourceAttr(resourceName, "ntp_timezone", "UTC"), + ), + }, + }, + }) +} + +func TestAccEngineConfiguration_validationErrors(t *testing.T) { + engineHost := "http://test-engine.example.com" + + resource.Test(t, resource.TestCase{ + PreCheck: func() { testAccPreCheck(t) }, + Providers: testAccProviders, + Steps: []resource.TestStep{ + { + Config: testAccEngineConfigurationObjectStorageMissingParams(engineHost), + ExpectError: regexp.MustCompile("object_storage_params must be provided when device_type is OBJECT"), + }, + { + Config: testAccEngineConfigurationObjectStorageMissingAccessKey(engineHost), + ExpectError: regexp.MustCompile("access_id and access_key must be provided when auth_type is ACCESS_KEY"), + }, + { + Config: testAccEngineConfigurationObjectStorageMissingNTP(engineHost), + ExpectError: regexp.MustCompile("ntp_servers and ntp_timezone must be provided when device_type is OBJECT"), + }, + { + Config: testAccEngineConfigurationInvalidStorageSize(engineHost), + ExpectError: regexp.MustCompile("must be a valid storage size with units"), + }, + }, + }) +} + +func TestValidateStorageSize(t *testing.T) { + validSizes := []string{ + "100GB", + "1.5TB", + "20TB", + "0.5PB", + "1000GB", + "2.5TB", + } + + invalidSizes := []string{ + "100", + "100MB", + "100KB", + "abc", + "100 GB", + "100gb", + "1.5.5TB", + "TB", + "", + } + + for _, size := range validSizes { + warnings, errors := validateStorageSize(size, "size") + if len(errors) > 0 { + t.Errorf("Expected %s to be valid, got errors: %v", size, errors) + } + if len(warnings) > 0 { + t.Errorf("Expected %s to have no warnings, got: %v", size, warnings) + } + } + + for _, size := range invalidSizes { + _, errors := validateStorageSize(size, "size") + if len(errors) == 0 { + t.Errorf("Expected %s to be invalid, but got no errors", size) + } + } +} + +func TestAccEngineConfiguration_comprehensive(t *testing.T) { + resourceName := "delphix_engine_configuration.test" + engineHost := os.Getenv("DELPHIX_ENGINE_HOST") + bucketName := os.Getenv("S3_BUCKET_NAME") + + if engineHost == "" || bucketName == "" { + t.Skip("DELPHIX_ENGINE_HOST or S3_BUCKET_NAME environment variable not set") + } + + resource.Test(t, resource.TestCase{ + PreCheck: func() { testAccPreCheck(t) }, + Providers: testAccProviders, + Steps: []resource.TestStep{ + { + Config: testAccEngineConfigurationComprehensive(engineHost, bucketName), + Check: resource.ComposeTestCheckFunc( + testAccCheckEngineConfigurationExists(resourceName), + resource.TestCheckResourceAttr(resourceName, "device_type", "OBJECT"), + // DNS Config + resource.TestCheckResourceAttr(resourceName, "dns_config.#", "1"), + resource.TestCheckResourceAttr(resourceName, "dns_config.0.servers.#", "3"), + // NTP Config + resource.TestCheckResourceAttr(resourceName, "ntp_servers.#", "2"), + resource.TestCheckResourceAttr(resourceName, "ntp_timezone", "America/New_York"), + // SMTP Config + resource.TestCheckResourceAttr(resourceName, "smtp_config.#", "1"), + resource.TestCheckResourceAttr(resourceName, "smtp_config.0.server", "smtp.example.com"), + // Web Proxy Config + resource.TestCheckResourceAttr(resourceName, "web_proxy_config.#", "1"), + resource.TestCheckResourceAttr(resourceName, "web_proxy_config.0.host", "proxy.internal.com"), + // Analytics and Phone Home + resource.TestCheckResourceAttr(resourceName, "phone_home_enabled", "true"), + resource.TestCheckResourceAttr(resourceName, "user_analytics_enabled", "true"), + // Object Storage + resource.TestCheckResourceAttr(resourceName, "object_storage_params.0.auth_type", "ROLE"), + ), + }, + }, + }) +} + +func testAccEngineConfigurationComprehensive(engineHost, bucketName string) string { + return fmt.Sprintf(` +resource "delphix_engine_configuration" "test" { + engine_host = "%s" + api_version = "1.11.46" + sys_user = "sysadmin" + sys_password = "sysadmin" + sys_new_password = "delphix" + user = "admin" + password = "delphix" + email = "test@example.com" + engine_type = "CD" + device_type = "OBJECT" + + # Object Storage Configuration + object_storage_params { + region = "us-west-2" + bucket = "%s" + endpoint = "s3.us-west-2.amazonaws.com" + size = "30GB" + auth_type = "ROLE" + } + + # NTP Configuration + ntp_servers = ["pool.ntp.org", "time.nist.gov"] + ntp_timezone = "America/New_York" + + # DNS Configuration + dns_config { + servers = ["172.16.105.22", "172.16.105.23", "8.8.8.8"] + domains = ["example.com", "internal.local", "test.local"] + } + + # SMTP Configuration + smtp_config { + server = "smtp.example.com" + port = 587 + from_email_address = "noreply@example.com" + tls_authentication = true + send_timeout = 120 + + smtp_authentication { + user = "smtp_user@example.com" + password = "smtp_password" + } + } + + # Web Proxy Configuration + web_proxy_config { + host = "proxy.internal.com" + port = 3128 + username = "proxy_admin" + password = "proxy_secret" + } + + # Analytics and Phone Home + phone_home_enabled = true + user_analytics_enabled = true +} +`, engineHost, bucketName) +} + +func testAccCheckEngineConfigurationExists(resourceName string) resource.TestCheckFunc { + return func(s *terraform.State) error { + rs, ok := s.RootModule().Resources[resourceName] + if !ok { + return fmt.Errorf("Resource not found: %s", resourceName) + } + + if rs.Primary.ID == "" { + return fmt.Errorf("No ID is set") + } + + // In a real scenario, you might make an API call here to verify the resource exists + return nil + } +} + +// Test configuration templates +func testAccEngineConfigurationBlockDevice(engineHost string) string { + return fmt.Sprintf(` +resource "delphix_engine_configuration" "test" { + engine_host = "%s" + api_version = "1.11.46" + sys_user = "sysadmin" + sys_password = "sysadmin" + user = "admin" + password = "delphix" + email = "test@example.com" + engine_type = "CD" + device_type = "BLOCK" +} +`, engineHost) +} + +func testAccEngineConfigurationObjectStorageRole(engineHost, bucketName string) string { + return fmt.Sprintf(` +resource "delphix_engine_configuration" "test" { + engine_host = "%s" + api_version = "1.11.46" + sys_user = "sysadmin" + sys_password = "sysadmin" + user = "admin" + password = "delphix" + email = "test@example.com" + engine_type = "CD" + device_type = "OBJECT" + + ntp_servers = ["pool.ntp.org", "time.nist.gov"] + ntp_timezone = "America/New_York" + + object_storage_params { + region = "us-west-2" + bucket = "%s" + endpoint = "s3.us-west-2.amazonaws.com" + size = "20GB" + auth_type = "ROLE" + } +} +`, engineHost, bucketName) +} + +func testAccEngineConfigurationObjectStorageAccessKey(engineHost, bucketName, accessId, accessKey string) string { + return fmt.Sprintf(` +resource "delphix_engine_configuration" "test" { + engine_host = "%s" + api_version = "1.11.46" + sys_user = "sysadmin" + sys_password = "sysadmin" + user = "admin" + password = "delphix" + email = "test@example.com" + engine_type = "CD" + device_type = "OBJECT" + + ntp_servers = ["pool.ntp.org", "time.nist.gov", "1.ubuntu.pool.ntp.org"] + ntp_timezone = "UTC" + + object_storage_params { + region = "us-west-2" + bucket = "%s" + endpoint = "s3.us-west-2.amazonaws.com" + size = "20GB" + auth_type = "ACCESS_KEY" + access_id = "%s" + access_key = "%s" + } +} +`, engineHost, bucketName, accessId, accessKey) +} + +func testAccEngineConfigurationObjectStorageMissingParams(engineHost string) string { + return fmt.Sprintf(` +resource "delphix_engine_configuration" "test" { + engine_host = "%s" + api_version = "1.11.46" + sys_user = "sysadmin" + sys_password = "sysadmin" + user = "admin" + password = "delphix" + email = "test@example.com" + engine_type = "CD" + device_type = "OBJECT" + # Missing object_storage_params +} +`, engineHost) +} + +func testAccEngineConfigurationObjectStorageMissingAccessKey(engineHost string) string { + return fmt.Sprintf(` +resource "delphix_engine_configuration" "test" { + engine_host = "%s" + api_version = "1.11.46" + sys_user = "sysadmin" + sys_password = "sysadmin" + user = "admin" + password = "delphix" + email = "test@example.com" + engine_type = "CD" + device_type = "OBJECT" + + ntp_servers = ["pool.ntp.org"] + ntp_timezone = "UTC" + + object_storage_params { + region = "us-west-2" + bucket = "test-bucket" + endpoint = "s3.us-west-2.amazonaws.com" + size = "20GB" + auth_type = "ACCESS_KEY" + # Missing access_id and access_key + } +} +`, engineHost) +} + +func testAccEngineConfigurationObjectStorageMissingNTP(engineHost string) string { + return fmt.Sprintf(` +resource "delphix_engine_configuration" "test" { + engine_host = "%s" + api_version = "1.11.46" + sys_user = "sysadmin" + sys_password = "sysadmin" + user = "admin" + password = "delphix" + email = "test@example.com" + engine_type = "CD" + device_type = "OBJECT" + + # Missing ntp_servers and ntp_timezone + + object_storage_params { + region = "us-west-2" + bucket = "test-bucket" + endpoint = "s3.us-west-2.amazonaws.com" + size = "20GB" + auth_type = "ROLE" + } +} +`, engineHost) +} + +func testAccEngineConfigurationInvalidStorageSize(engineHost string) string { + return fmt.Sprintf(` +resource "delphix_engine_configuration" "test" { + engine_host = "%s" + api_version = "1.11.46" + sys_user = "sysadmin" + sys_password = "sysadmin" + user = "admin" + password = "delphix" + email = "test@example.com" + engine_type = "CD" + device_type = "OBJECT" + + ntp_servers = ["pool.ntp.org"] + ntp_timezone = "UTC" + + object_storage_params { + region = "us-west-2" + bucket = "test-bucket" + endpoint = "s3.us-west-2.amazonaws.com" + size = "20MB" # Invalid size unit + auth_type = "ROLE" + } +} +`, engineHost) +} diff --git a/internal/provider/resource_engine_registration.go b/internal/provider/resource_engine_registration.go index 7827e8e..03dbd85 100644 --- a/internal/provider/resource_engine_registration.go +++ b/internal/provider/resource_engine_registration.go @@ -35,8 +35,9 @@ func resourceEngineRegistration() *schema.Resource { Required: true, }, "password": { - Type: schema.TypeString, - Required: true, + Type: schema.TypeString, + Required: true, + Sensitive: true, }, "masking_username": { Type: schema.TypeString, @@ -297,17 +298,6 @@ func resourceEngineRegistrationDelete(ctx context.Context, d *schema.ResourceDat return diags } - // job_status, job_err := PollJobStatus(*apiRes.Job.Id, ctx, client) - // if job_err != "" { - // tflog.Error(ctx, DLPX+ERROR+"Job Polling failed but continuing with engine removal. Error: "+job_err) - // } - // if isJobTerminalFailure(job_status) { - // return diag.Errorf("[NOT OK] Engine-Delete %s. JobId: %s / Error: %s", job_status, *apiRes.Job.Id, job_err) - // } - // _, diags := PollForObjectDeletion(ctx, func() (interface{}, *http.Response, error) { - // return client.ManagementApi.GetRegisteredEngine(ctx, engineID).Execute() - // }) - _, diags := PollForObjectExistence(ctx, func() (interface{}, *http.Response, error) { return client.ManagementAPI.GetRegisteredEngine(ctx, engineID).Execute() }) diff --git a/internal/provider/resource_engine_registration_test.go b/internal/provider/resource_engine_registration_test.go new file mode 100644 index 0000000..46252b9 --- /dev/null +++ b/internal/provider/resource_engine_registration_test.go @@ -0,0 +1,226 @@ +package provider + +import ( + "context" + "fmt" + "math/rand" + "os" + "regexp" + "testing" + "time" + + "github.com/hashicorp/terraform-plugin-sdk/v2/helper/resource" + "github.com/hashicorp/terraform-plugin-sdk/v2/terraform" +) + +func TestAccEngineRegistration_withSSLConfig(t *testing.T) { + resourceName := "delphix_engine_dct_registration.test" + engineName := "test-engine-ssl-" + randomString(8) + engineHostname := os.Getenv("TEST_ENGINE_HOSTNAME") + engineUsername := os.Getenv("TEST_ENGINE_USERNAME") + enginePassword := os.Getenv("TEST_ENGINE_PASSWORD") + + if engineHostname == "" || engineUsername == "" || enginePassword == "" { + t.Skip("Required environment variables not set") + } + + resource.Test(t, resource.TestCase{ + PreCheck: func() { testAccPreCheck(t) }, + Providers: testAccProviders, + CheckDestroy: testAccCheckEngineRegistrationDestroy, + Steps: []resource.TestStep{ + { + Config: testAccEngineRegistrationWithSSL( + engineName, engineHostname, engineUsername, enginePassword, + ), + Check: resource.ComposeTestCheckFunc( + testAccCheckEngineRegistrationExists(resourceName), + resource.TestCheckResourceAttr(resourceName, "name", engineName), + resource.TestCheckResourceAttr(resourceName, "insecure_ssl", "true"), + resource.TestCheckResourceAttr(resourceName, "unsafe_ssl_hostname_check", "true"), + resource.TestCheckResourceAttrSet(resourceName, "id"), + ), + }, + }, + }) +} + +func TestAccEngineRegistration_withTags(t *testing.T) { + resourceName := "delphix_engine_dct_registration.test" + engineName := "test-engine-tags-" + randomString(8) + engineHostname := os.Getenv("TEST_ENGINE_HOSTNAME") + engineUsername := os.Getenv("TEST_ENGINE_USERNAME") + enginePassword := os.Getenv("TEST_ENGINE_PASSWORD") + + if engineHostname == "" || engineUsername == "" || enginePassword == "" { + t.Skip("Required environment variables not set") + } + + resource.Test(t, resource.TestCase{ + PreCheck: func() { testAccPreCheck(t) }, + Providers: testAccProviders, + CheckDestroy: testAccCheckEngineRegistrationDestroy, + Steps: []resource.TestStep{ + { + Config: testAccEngineRegistrationWithTags( + engineName, engineHostname, engineUsername, enginePassword, + ), + Check: resource.ComposeTestCheckFunc( + testAccCheckEngineRegistrationExists(resourceName), + resource.TestCheckResourceAttr(resourceName, "name", engineName), + resource.TestCheckResourceAttr(resourceName, "tags.#", "2"), + resource.TestCheckResourceAttr(resourceName, "tags.0.key", "environment"), + resource.TestCheckResourceAttr(resourceName, "tags.0.value", "test"), + resource.TestCheckResourceAttr(resourceName, "tags.1.key", "team"), + resource.TestCheckResourceAttr(resourceName, "tags.1.value", "qa"), + resource.TestCheckResourceAttrSet(resourceName, "id"), + ), + }, + }, + }) +} + +func TestAccEngineRegistration_validationErrors(t *testing.T) { + resource.Test(t, resource.TestCase{ + PreCheck: func() { testAccPreCheck(t) }, + Providers: testAccProviders, + Steps: []resource.TestStep{ + { + Config: testAccEngineRegistrationMissingName(), + ExpectError: regexp.MustCompile("The argument \"name\" is required"), + }, + { + Config: testAccEngineRegistrationMissingHostname(), + ExpectError: regexp.MustCompile("The argument \"hostname\" is required"), + }, + { + Config: testAccEngineRegistrationMissingCredentials(), + ExpectError: regexp.MustCompile("The argument \"username\" is required"), + }, + }, + }) +} + +func testAccCheckEngineRegistrationExists(resourceName string) resource.TestCheckFunc { + return func(s *terraform.State) error { + rs, ok := s.RootModule().Resources[resourceName] + if !ok { + return fmt.Errorf("Resource not found: %s", resourceName) + } + + if rs.Primary.ID == "" { + return fmt.Errorf("No ID is set") + } + + client := testAccProvider.Meta().(*apiClient).client + _, _, err := client.ManagementAPI.GetRegisteredEngine(context.Background(), rs.Primary.ID).Execute() + if err != nil { + return fmt.Errorf("Error getting registered engine: %s", err) + } + + return nil + } +} + +func testAccCheckEngineRegistrationDestroy(s *terraform.State) error { + client := testAccProvider.Meta().(*apiClient).client + time.Sleep(time.Duration(60) * time.Second) + for _, rs := range s.RootModule().Resources { + if rs.Type != "delphix_engine_dct_registration" { + continue + } + + _, httpRes, err := client.ManagementAPI.GetRegisteredEngine(context.Background(), rs.Primary.ID).Execute() + if err == nil { + return fmt.Errorf("Engine registration still exists") + } + + // Check if it's a 404 - which means the resource was successfully deleted + if httpRes != nil && httpRes.StatusCode == 404 { + continue + } + + // If we get any other error, return it + return fmt.Errorf("Unexpected error checking for deleted engine registration: %s", err) + } + + return nil +} + +// Test configuration templates + +func testAccEngineRegistrationWithSSL(name, hostname, username, password string) string { + return fmt.Sprintf(` +resource "delphix_engine_dct_registration" "test" { + name = "%s" + hostname = "%s" + username = "%s" + password = "%s" + insecure_ssl = true + unsafe_ssl_hostname_check = true +} +`, name, hostname, username, password) +} + +// Validation error test configurations + +func testAccEngineRegistrationMissingName() string { + return ` +resource "delphix_engine_dct_registration" "test" { + hostname = "test.example.com" + username = "admin" + password = "password" +} +` +} + +func testAccEngineRegistrationMissingHostname() string { + return ` +resource "delphix_engine_dct_registration" "test" { + name = "test-engine" + username = "admin" + password = "password" +} +` +} + +func testAccEngineRegistrationMissingCredentials() string { + return ` +resource "delphix_engine_dct_registration" "test" { + name = "test-engine" + hostname = "test.example.com" + password = "password" +} +` +} + +// Utility function to generate random strings for unique resource names +func randomString(length int) string { + const charset = "abcdefghijklmnopqrstuvwxyz0123456789" + result := make([]byte, length) + for i := range result { + result[i] = charset[rand.Intn(len(charset))] + } + return string(result) +} + +func testAccEngineRegistrationWithTags(name, hostname, username, password string) string { + return fmt.Sprintf(` +resource "delphix_engine_dct_registration" "test" { + name = "%s" + hostname = "%s" + username = "%s" + password = "%s" + insecure_ssl = true + tags { + key = "environment" + value = "test" + } + + tags { + key = "team" + value = "qa" + } +} +`, name, hostname, username, password) +} diff --git a/internal/provider/resource_engine_user_management.go b/internal/provider/resource_engine_user_management.go deleted file mode 100644 index 35b09d2..0000000 --- a/internal/provider/resource_engine_user_management.go +++ /dev/null @@ -1,198 +0,0 @@ -package provider - -import ( - "context" - "encoding/json" - "net/http" - "net/http/cookiejar" - - "github.com/hashicorp/terraform-plugin-log/tflog" - "github.com/hashicorp/terraform-plugin-sdk/v2/diag" - "github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema" -) - -func resourceEngineUserManagement() *schema.Resource { - return &schema.Resource{ - // This description is used by the documentation generator and the language server. - Description: "Resource for Engine User Management.", - - CreateContext: engineUserCreate, - ReadContext: engineUserRead, - UpdateContext: engineUserUpdate, - DeleteContext: engineUserDelete, - - Schema: map[string]*schema.Schema{ - "engine_host": { - Type: schema.TypeString, - Required: true, - }, - "version": { - Type: schema.TypeString, - Required: true, - }, - "login_user": { - Type: schema.TypeString, - Required: true, - Sensitive: true, - }, - "login_password": { - Type: schema.TypeString, - Required: true, - Sensitive: true, - }, - "user_name": { - Type: schema.TypeString, - Required: true, - Sensitive: true, - }, - "password": { - Type: schema.TypeString, - Required: true, - Sensitive: true, - }, - "first_name": { - Type: schema.TypeString, - Optional: true, - }, - "last_name": { - Type: schema.TypeString, - Optional: true, - }, - "email": { - Type: schema.TypeString, - Optional: true, - }, - "user_type": { - Type: schema.TypeString, - Optional: true, - }, - }, - } // maye be add enabled and other phone no feilds -} - -func engineUserCreate(ctx context.Context, d *schema.ResourceData, meta interface{}) diag.Diagnostics { - var diags diag.Diagnostics - - // Create a cookie jar to store session cookies - jar, _ := cookiejar.New(nil) - client := &http.Client{Jar: jar} - - engine_host, _ := d.Get("engine_host").(string) - version, _ := d.Get("version").(string) - login_user, _ := d.Get("login_user").(string) - login_password, _ := d.Get("login_password").(string) - user_name, _ := d.Get("user_name").(string) - password, _ := d.Get("password").(string) - user_type, _ := d.Get("user_type").(string) - - // Start a session - tflog.Info(ctx, DLPX+INFO+"start Session for "+engine_host) - err := startSession(ctx, client, engine_host, version) - if err != nil { - return diag.Errorf("Error starting session: %s", err) - } - - // Authenticate/login - tflog.Info(ctx, DLPX+INFO+"login as "+login_user) - err = login(ctx, client, engine_host, login_user, login_password, user_type) - if err != nil { - return diag.Errorf("Error logging in: %s", err) - } - - //Add check to see if it is already existing user - - action, err := createOrUpdateUser(ctx, client, engine_host, user_name, password, user_type) - if err != nil { - return diag.Errorf("Error logging in: %s", err) - } - - var result ActionResult - unmarshalErr := json.Unmarshal(action, &result) - if unmarshalErr != nil { - tflog.Error(ctx, DLPX+ERROR+"Error unmarshalling: "+unmarshalErr.Error()) - } - - tflog.Info(ctx, DLPX+INFO+"User Create Successfull!") - - d.SetId(engine_host) - - readDiags := engineUserRead(ctx, d, meta) - if readDiags.HasError() { - return readDiags - } - - return diags -} - -func engineUserUpdate(ctx context.Context, d *schema.ResourceData, meta interface{}) diag.Diagnostics { - // get the changed keys - changedKeys := make([]string, 0, len(d.State().Attributes)) - for k := range d.State().Attributes { - if d.HasChange(k) { - changedKeys = append(changedKeys, k) - } - } - // revert and set the old value to the changed keys - for _, key := range changedKeys { - old, _ := d.GetChange(key) - d.Set(key, old) - } - - return diag.Errorf("Action update not available for engine config : dSource") -} - -func engineUserRead(ctx context.Context, d *schema.ResourceData, meta interface{}) diag.Diagnostics { - var diags diag.Diagnostics - - engineId := d.Id() - version, _ := d.Get("version").(string) - - // Create a cookie jar to store session cookies - jar, _ := cookiejar.New(nil) - client := &http.Client{Jar: jar} - - // Start a session - err := startSession(ctx, client, engineId, version) - if err != nil { - diag.Errorf("Error starting session: %v", err) - } - - // Authenticate/login - err = login(ctx, client, engineId, d.Get("sys_user").(string), d.Get("sys_new_password").(string), "SYSTEM") - if err != nil { - diag.Errorf("Error logging in: %v", err) - } - - body, err := getSystem(ctx, client, engineId) - if err != nil { - diag.Errorf("Error getting system info: %v", err) - } - - var response SystemInfoResponse - sysErr := json.Unmarshal(body, &response) - if sysErr != nil { - tflog.Error(ctx, DLPX+ERROR+"Error unmarshalling", map[string]interface{}{"error": sysErr.Error()}) - } - - d.Set("configured", response.Result["configured"]) - d.Set("hostname", response.Result["hostname"]) - - return diags -} - -func engineUserDelete(ctx context.Context, d *schema.ResourceData, meta interface{}) diag.Diagnostics { - // get the changed keys - changedKeys := make([]string, 0, len(d.State().Attributes)) - for k := range d.State().Attributes { - if d.HasChange(k) { - changedKeys = append(changedKeys, k) - } - } - // revert and set the old value to the changed keys - for _, key := range changedKeys { - old, _ := d.GetChange(key) - d.Set(key, old) - } - - return diag.Errorf("Action delete not available for engine config : dSource") -}