zone.go raw

   1  package dns
   2  
   3  import (
   4  	"bytes"
   5  	"context"
   6  	"encoding/json"
   7  	"errors"
   8  	"fmt"
   9  	"io"
  10  	"net/http"
  11  	"net/url"
  12  	"reflect"
  13  	"strconv"
  14  	"strings"
  15  	"sync"
  16  	"time"
  17  
  18  	"github.com/akamai/AkamaiOPEN-edgegrid-golang/v11/pkg/edgegriderr"
  19  	"github.com/akamai/AkamaiOPEN-edgegrid-golang/v11/pkg/session"
  20  	validation "github.com/go-ozzo/ozzo-validation/v4"
  21  )
  22  
  23  var (
  24  	zoneWriteLock sync.Mutex
  25  )
  26  
  27  type (
  28  	// ZoneQueryString contains zone query parameters
  29  	ZoneQueryString struct {
  30  		Contract string
  31  		Group    string
  32  	}
  33  
  34  	// ZoneCreate contains zone create request
  35  	ZoneCreate struct {
  36  		Zone                  string                `json:"zone"`
  37  		Type                  string                `json:"type"`
  38  		Masters               []string              `json:"masters,omitempty"`
  39  		Comment               string                `json:"comment,omitempty"`
  40  		SignAndServe          bool                  `json:"signAndServe"`
  41  		SignAndServeAlgorithm string                `json:"signAndServeAlgorithm,omitempty"`
  42  		TSIGKey               *TSIGKey              `json:"tsigKey,omitempty"`
  43  		Target                string                `json:"target,omitempty"`
  44  		EndCustomerID         string                `json:"endCustomerId,omitempty"`
  45  		ContractID            string                `json:"contractId,omitempty"`
  46  		OutboundZoneTransfer  *OutboundZoneTransfer `json:"outboundZoneTransfer,omitempty"`
  47  	}
  48  
  49  	// OutboundZoneTransfer contains OutboundZoneTransfer request parameters
  50  	OutboundZoneTransfer struct {
  51  		ACL           []string `json:"ACL"`
  52  		Enabled       bool     `json:"enabled"`
  53  		NotifyTargets []string `json:"notifyTargets"`
  54  		TSIGKey       *TSIGKey `json:"tsigKey,omitempty"`
  55  	}
  56  
  57  	// ZoneResponse contains zone create response
  58  	ZoneResponse struct {
  59  		Zone                  string                `json:"zone,omitempty"`
  60  		Type                  string                `json:"type,omitempty"`
  61  		Masters               []string              `json:"masters,omitempty"`
  62  		Comment               string                `json:"comment,omitempty"`
  63  		SignAndServe          bool                  `json:"signAndServe"`
  64  		SignAndServeAlgorithm string                `json:"signAndServeAlgorithm,omitempty"`
  65  		TSIGKey               *TSIGKey              `json:"tsigKey,omitempty"`
  66  		Target                string                `json:"target,omitempty"`
  67  		EndCustomerID         string                `json:"endCustomerId,omitempty"`
  68  		ContractID            string                `json:"contractId,omitempty"`
  69  		AliasCount            int64                 `json:"aliasCount,omitempty"`
  70  		ActivationState       string                `json:"activationState,omitempty"`
  71  		LastActivationDate    string                `json:"lastActivationDate,omitempty"`
  72  		LastModifiedBy        string                `json:"lastModifiedBy,omitempty"`
  73  		LastModifiedDate      string                `json:"lastModifiedDate,omitempty"`
  74  		VersionID             string                `json:"versionId,omitempty"`
  75  		OutboundZoneTransfer  *OutboundZoneTransfer `json:"outboundZoneTransfer,omitempty"`
  76  	}
  77  
  78  	// ListMetadata contains metadata for List Zones request
  79  	ListMetadata struct {
  80  		ContractIDs   []string `json:"contractIds"`
  81  		Page          int      `json:"page"`
  82  		PageSize      int      `json:"pageSize"`
  83  		ShowAll       bool     `json:"showAll"`
  84  		TotalElements int      `json:"totalElements"`
  85  	}
  86  
  87  	// ZoneListResponse contains response for List Zones request
  88  	ZoneListResponse struct {
  89  		Metadata *ListMetadata  `json:"metadata,omitempty"`
  90  		Zones    []ZoneResponse `json:"zones,omitempty"`
  91  	}
  92  
  93  	// ZoneRequest contains request parameters
  94  	ZoneRequest struct {
  95  		Zone string
  96  	}
  97  
  98  	// GetZoneResponse contains the response data from GetZone operation
  99  	GetZoneResponse ZoneResponse
 100  
 101  	// GetChangeListResponse contains metadata about a change list
 102  	GetChangeListResponse struct {
 103  		Zone             string `json:"zone,omitempty"`
 104  		ChangeTag        string `json:"changeTag,omitempty"`
 105  		ZoneVersionID    string `json:"zoneVersionId,omitempty"`
 106  		LastModifiedDate string `json:"lastModifiedDate,omitempty"`
 107  		Stale            bool   `json:"stale,omitempty"`
 108  	}
 109  
 110  	// ZoneNameListResponse contains response with a list of zone's names and aliases
 111  	ZoneNameListResponse struct {
 112  		Zones   []string `json:"zones"`
 113  		Aliases []string `json:"aliases,omitempty"`
 114  	}
 115  
 116  	// GetZoneNamesResponse contains record set names for zone
 117  	GetZoneNamesResponse struct {
 118  		Names []string `json:"names"`
 119  	}
 120  
 121  	// GetZoneNameTypesResponse contains record set types for zone
 122  	GetZoneNameTypesResponse struct {
 123  		Types []string `json:"types"`
 124  	}
 125  	// GetZoneRequest contains request parameters for GetZone
 126  	GetZoneRequest ZoneRequest
 127  
 128  	// GetChangeListRequest contains request parameters for GetChangeList
 129  	GetChangeListRequest ZoneRequest
 130  
 131  	// ListZonesRequest contains request parameters for ListZones
 132  	ListZonesRequest struct {
 133  		ContractIDs string
 134  		Page        int
 135  		PageSize    int
 136  		Search      string
 137  		ShowAll     bool
 138  		SortBy      string
 139  		Types       string
 140  	}
 141  	// GetMasterZoneFileRequest contains request parameters for GetMasterZoneFile
 142  	GetMasterZoneFileRequest ZoneRequest
 143  
 144  	// PostMasterZoneFileRequest contains request parameters for PostMasterZoneFile
 145  	PostMasterZoneFileRequest struct {
 146  		Zone     string
 147  		FileData string
 148  	}
 149  	// CreateZoneRequest contains request parameters for CreateZone
 150  	CreateZoneRequest struct {
 151  		CreateZone      *ZoneCreate
 152  		ZoneQueryString ZoneQueryString
 153  		ClearConn       []bool
 154  	}
 155  	// SaveChangeListRequest contains request parameters for SaveChangelist
 156  	SaveChangeListRequest ZoneCreate
 157  
 158  	// SubmitChangeListRequest contains request parameters for SubmitChangeList
 159  	SubmitChangeListRequest ZoneCreate
 160  
 161  	// UpdateZoneRequest contains request parameters for UpdateZone
 162  	UpdateZoneRequest struct {
 163  		CreateZone *ZoneCreate
 164  	}
 165  	// GetZoneNamesRequest contains request parameters for GetZoneNames
 166  	GetZoneNamesRequest ZoneRequest
 167  
 168  	// GetZoneNameTypesRequest contains request parameters for GetZoneNameTypes
 169  	GetZoneNameTypesRequest struct {
 170  		Zone     string
 171  		ZoneName string
 172  	}
 173  
 174  	// GetZonesDNSSecStatusRequest is used to get the DNSSEC status for one or more zones
 175  	GetZonesDNSSecStatusRequest struct {
 176  		Zones []string `json:"zones"`
 177  	}
 178  
 179  	// GetZonesDNSSecStatusResponse represents a list of DNSSEC statuses for DNS zones specified
 180  	// in the GetZonesDNSSecStatus request
 181  	GetZonesDNSSecStatusResponse struct {
 182  		DNSSecStatuses []SecStatus `json:"dnsSecStatuses"`
 183  	}
 184  
 185  	// SecStatus represents the DNSSEC status for a DNS zone
 186  	SecStatus struct {
 187  		Zone           string      `json:"zone"`
 188  		Alerts         []string    `json:"alerts"`
 189  		CurrentRecords SecRecords  `json:"currentRecords"`
 190  		NewRecords     *SecRecords `json:"newRecords"`
 191  	}
 192  
 193  	// SecRecords represents a set of DNSSEC records for a DNS zone
 194  	SecRecords struct {
 195  		DNSKeyRecord     string    `json:"dnskeyRecord"`
 196  		DSRecord         string    `json:"dsRecord"`
 197  		ExpectedTTL      int64     `json:"expectedTtl"`
 198  		LastModifiedDate time.Time `json:"lastModifiedDate"`
 199  	}
 200  )
 201  
 202  var (
 203  	// ErrGetZone is returned when GetZone fails
 204  	ErrGetZone = errors.New("get zone")
 205  	// ErrGetChangeList is returned when GetChangeList fails
 206  	ErrGetChangeList = errors.New("get change list")
 207  	// ErrGetMasterZoneFile is returned when GetMasterZoneFile fails
 208  	ErrGetMasterZoneFile = errors.New("get master zone file")
 209  	// ErrPostMasterZoneFile is returned when PostMasterZoneFile fails
 210  	ErrPostMasterZoneFile = errors.New("post master zone file")
 211  	// ErrCreateZone is returned when CreateZone fails
 212  	ErrCreateZone = errors.New("create zone")
 213  	// ErrSaveChangeList is returned when SaveChangeList fails
 214  	ErrSaveChangeList = errors.New("save change list")
 215  	// ErrSubmitChangeList is returned when SubmitChangeList fails
 216  	ErrSubmitChangeList = errors.New("submit change list")
 217  	// ErrGetZoneNames is returned when GetZoneNames fails
 218  	ErrGetZoneNames = errors.New("get zone names")
 219  	// ErrGetZoneNameTypes is returned when GetZoneNameTypes fails
 220  	ErrGetZoneNameTypes = errors.New("get zone name types")
 221  )
 222  
 223  // Validate validates GetZoneNameTypesRequest
 224  func (r GetZoneNameTypesRequest) Validate() error {
 225  	return edgegriderr.ParseValidationErrors(validation.Errors{
 226  		"Zone":     validation.Validate(r.Zone, validation.Required),
 227  		"ZoneName": validation.Validate(r.ZoneName, validation.Required),
 228  	})
 229  }
 230  
 231  // Validate validates GetZoneNamesRequest
 232  func (r GetZoneNamesRequest) Validate() error {
 233  	return edgegriderr.ParseValidationErrors(validation.Errors{
 234  		"Zone": validation.Validate(r.Zone, validation.Required),
 235  	})
 236  }
 237  
 238  // Validate validates SubmitChangeListRequest
 239  func (r SubmitChangeListRequest) Validate() error {
 240  	return edgegriderr.ParseValidationErrors(validation.Errors{
 241  		"Zone": validation.Validate(r.Zone, validation.Required),
 242  	})
 243  }
 244  
 245  // Validate validates SaveChangelistRequest
 246  func (r SaveChangeListRequest) Validate() error {
 247  	return edgegriderr.ParseValidationErrors(validation.Errors{
 248  		"Zone": validation.Validate(r.Zone, validation.Required),
 249  	})
 250  }
 251  
 252  // Validate validates PostMasterZoneFileRequest
 253  func (r PostMasterZoneFileRequest) Validate() error {
 254  	return edgegriderr.ParseValidationErrors(validation.Errors{
 255  		"Zone": validation.Validate(r.Zone, validation.Required),
 256  	})
 257  }
 258  
 259  // Validate validates CreateZoneRequest
 260  func (r CreateZoneRequest) Validate() error {
 261  	return edgegriderr.ParseValidationErrors(validation.Errors{
 262  		"ZoneQueryString": validation.Validate(r.ZoneQueryString, validation.Required),
 263  	})
 264  }
 265  
 266  // Validate validates GetZoneRequest
 267  func (r GetZoneRequest) Validate() error {
 268  	return edgegriderr.ParseValidationErrors(validation.Errors{
 269  		"Zone": validation.Validate(r.Zone, validation.Required),
 270  	})
 271  }
 272  
 273  // Validate validates GetMasterZoneFileRequest
 274  func (r GetMasterZoneFileRequest) Validate() error {
 275  	return edgegriderr.ParseValidationErrors(validation.Errors{
 276  		"Zone": validation.Validate(r.Zone, validation.Required),
 277  	})
 278  }
 279  
 280  // Validate validates GetChangeListRequest
 281  func (r GetChangeListRequest) Validate() error {
 282  	return edgegriderr.ParseValidationErrors(validation.Errors{
 283  		"Zone": validation.Validate(r.Zone, validation.Required),
 284  	})
 285  }
 286  
 287  // Validate validates GetZonesDNSSecStatusRequest
 288  func (r GetZonesDNSSecStatusRequest) Validate() error {
 289  	return edgegriderr.ParseValidationErrors(validation.Errors{
 290  		"Zones": validation.Validate(r.Zones, validation.Required),
 291  	})
 292  }
 293  
 294  var zoneStructMap = map[string]string{
 295  	"Zone":                  "zone",
 296  	"Type":                  "type",
 297  	"Masters":               "masters",
 298  	"Comment":               "comment",
 299  	"SignAndServe":          "signAndServe",
 300  	"SignAndServeAlgorithm": "signAndServeAlgorithm",
 301  	"TSIGKey":               "tsigKey",
 302  	"Target":                "target",
 303  	"EndCustomerID":         "endCustomerId",
 304  	"OutboundZoneTransfer":  "outboundZoneTransfer",
 305  	"ContractID":            "contractId"}
 306  
 307  // Util to convert struct to http request body, e.g. io.reader
 308  func convertStructToReqBody(srcStruct interface{}) (io.Reader, error) {
 309  	reqBody, err := json.Marshal(srcStruct)
 310  	if err != nil {
 311  		return nil, err
 312  	}
 313  	return bytes.NewBuffer(reqBody), nil
 314  }
 315  
 316  func (d *dns) ListZones(ctx context.Context, params ListZonesRequest) (*ZoneListResponse, error) {
 317  	logger := d.Log(ctx)
 318  	logger.Debug("ListZones")
 319  
 320  	getURL := "/config-dns/v2/zones"
 321  
 322  	req, err := http.NewRequestWithContext(ctx, http.MethodGet, getURL, nil)
 323  	if err != nil {
 324  		return nil, fmt.Errorf("failed to create listzones request: %w", err)
 325  	}
 326  
 327  	q := req.URL.Query()
 328  	if params.Page > 0 {
 329  		q.Add("page", strconv.Itoa(params.Page))
 330  	}
 331  	if params.PageSize > 0 {
 332  		q.Add("pageSize", strconv.Itoa(params.PageSize))
 333  	}
 334  	if params.Search != "" {
 335  		q.Add("search", params.Search)
 336  	}
 337  	q.Add("showAll", strconv.FormatBool(params.ShowAll))
 338  	if params.SortBy != "" {
 339  		q.Add("sortBy", params.SortBy)
 340  	}
 341  	if params.Types != "" {
 342  		q.Add("types", params.Types)
 343  	}
 344  	if params.ContractIDs != "" {
 345  		q.Add("contractIds", params.ContractIDs)
 346  	}
 347  	req.URL.RawQuery = q.Encode()
 348  
 349  	var result ZoneListResponse
 350  	resp, err := d.Exec(req, &result)
 351  	if err != nil {
 352  		return nil, fmt.Errorf("listzones request failed: %w", err)
 353  	}
 354  	defer session.CloseResponseBody(resp)
 355  
 356  	if resp.StatusCode != http.StatusOK {
 357  		return nil, d.Error(resp)
 358  	}
 359  
 360  	return &result, nil
 361  }
 362  
 363  func (d *dns) GetZone(ctx context.Context, params GetZoneRequest) (*GetZoneResponse, error) {
 364  	logger := d.Log(ctx)
 365  	logger.Debug("GetZone")
 366  
 367  	if err := params.Validate(); err != nil {
 368  		return nil, fmt.Errorf("%s: %w: %s", ErrGetZone, ErrStructValidation, err)
 369  	}
 370  
 371  	getURL := fmt.Sprintf("/config-dns/v2/zones/%s", params.Zone)
 372  	req, err := http.NewRequestWithContext(ctx, http.MethodGet, getURL, nil)
 373  	if err != nil {
 374  		return nil, fmt.Errorf("failed to create GetZone request: %w", err)
 375  	}
 376  
 377  	var result GetZoneResponse
 378  	resp, err := d.Exec(req, &result)
 379  	if err != nil {
 380  		return nil, fmt.Errorf("GetZone request failed: %w", err)
 381  	}
 382  	defer session.CloseResponseBody(resp)
 383  
 384  	if resp.StatusCode != http.StatusOK {
 385  		return nil, d.Error(resp)
 386  	}
 387  
 388  	return &result, nil
 389  }
 390  
 391  func (d *dns) GetChangeList(ctx context.Context, params GetChangeListRequest) (*GetChangeListResponse, error) {
 392  	logger := d.Log(ctx)
 393  	logger.Debug("GetChangeList")
 394  
 395  	if err := params.Validate(); err != nil {
 396  		return nil, fmt.Errorf("%s: %w: %s", ErrGetChangeList, ErrStructValidation, err)
 397  	}
 398  
 399  	getURL := fmt.Sprintf("/config-dns/v2/changelists/%s", params.Zone)
 400  	req, err := http.NewRequestWithContext(ctx, http.MethodGet, getURL, nil)
 401  	if err != nil {
 402  		return nil, fmt.Errorf("failed to create GetChangeList request: %w", err)
 403  	}
 404  
 405  	var result GetChangeListResponse
 406  	resp, err := d.Exec(req, &result)
 407  	if err != nil {
 408  		return nil, fmt.Errorf("GetChangeList request failed: %w", err)
 409  	}
 410  	defer session.CloseResponseBody(resp)
 411  
 412  	if resp.StatusCode != http.StatusOK {
 413  		return nil, d.Error(resp)
 414  	}
 415  
 416  	return &result, nil
 417  }
 418  
 419  func (d *dns) GetMasterZoneFile(ctx context.Context, params GetMasterZoneFileRequest) (string, error) {
 420  	logger := d.Log(ctx)
 421  	logger.Debug("GetMasterZoneFile")
 422  
 423  	if err := params.Validate(); err != nil {
 424  		return "", fmt.Errorf("%s: %w: %s", ErrGetMasterZoneFile, ErrStructValidation, err)
 425  	}
 426  
 427  	getURL := fmt.Sprintf("/config-dns/v2/zones/%s/zone-file", params.Zone)
 428  	req, err := http.NewRequestWithContext(ctx, http.MethodGet, getURL, nil)
 429  	if err != nil {
 430  		return "", fmt.Errorf("failed to create GetMasterZoneFile request: %w", err)
 431  	}
 432  	req.Header.Add("Accept", "text/dns")
 433  
 434  	resp, err := d.Exec(req, nil)
 435  	if err != nil {
 436  		return "", fmt.Errorf("GetMasterZoneFile request failed: %w", err)
 437  	}
 438  	defer session.CloseResponseBody(resp)
 439  
 440  	if resp.StatusCode != http.StatusOK {
 441  		return "", d.Error(resp)
 442  	}
 443  
 444  	masterFile, err := io.ReadAll(resp.Body)
 445  	if err != nil {
 446  		return "", fmt.Errorf("GetMasterZoneFile request failed: %w", err)
 447  	}
 448  
 449  	return string(masterFile), nil
 450  }
 451  
 452  func (d *dns) PostMasterZoneFile(ctx context.Context, params PostMasterZoneFileRequest) error {
 453  	logger := d.Log(ctx)
 454  	logger.Debug("PostMasterZoneFile")
 455  
 456  	if err := params.Validate(); err != nil {
 457  		return fmt.Errorf("%s: %w: %s", ErrPostMasterZoneFile, ErrStructValidation, err)
 458  	}
 459  
 460  	mtResp := ""
 461  	pmzfURL := fmt.Sprintf("/config-dns/v2/zones/%s/zone-file", params.Zone)
 462  	buf := bytes.NewReader([]byte(params.FileData))
 463  	req, err := http.NewRequestWithContext(ctx, http.MethodPost, pmzfURL, buf)
 464  	if err != nil {
 465  		return fmt.Errorf("failed to create PostMasterZoneFile request: %w", err)
 466  	}
 467  
 468  	req.Header.Set("Content-Type", "text/dns")
 469  
 470  	resp, err := d.Exec(req, &mtResp)
 471  	if err != nil {
 472  		return fmt.Errorf("Create PostMasterZoneFile failed: %w", err)
 473  	}
 474  	defer session.CloseResponseBody(resp)
 475  
 476  	if resp.StatusCode != http.StatusNoContent {
 477  		return d.Error(resp)
 478  	}
 479  
 480  	return nil
 481  }
 482  
 483  func (d *dns) CreateZone(ctx context.Context, params CreateZoneRequest) error {
 484  	// This lock will restrict the concurrency of API calls
 485  	// to 1 save request at a time. This is needed for the Soa.Serial value which
 486  	// is required to be incremented for every subsequent update to a zone,
 487  	// so we have to save just one request at a time to ensure this is always
 488  	// incremented properly
 489  
 490  	zoneWriteLock.Lock()
 491  	defer zoneWriteLock.Unlock()
 492  
 493  	logger := d.Log(ctx)
 494  	logger.Debug("Zone Create")
 495  
 496  	if err := params.Validate(); err != nil {
 497  		return fmt.Errorf("%s: %w: %s", ErrCreateZone, ErrStructValidation, err)
 498  	}
 499  
 500  	if err := ValidateZone(params.CreateZone); err != nil {
 501  		return err
 502  	}
 503  
 504  	uri, err := url.Parse("/config-dns/v2/zones")
 505  	if err != nil {
 506  		return fmt.Errorf("%w: failed to parse uri: %s", ErrCreateZone, err)
 507  	}
 508  
 509  	q := uri.Query()
 510  	q.Add("contractId", params.ZoneQueryString.Contract)
 511  	if params.ZoneQueryString.Group != "" {
 512  		q.Add("gid", params.ZoneQueryString.Group)
 513  	}
 514  	uri.RawQuery = q.Encode()
 515  
 516  	req, err := http.NewRequestWithContext(ctx, http.MethodPost, uri.String(), nil)
 517  	if err != nil {
 518  		return fmt.Errorf("failed to create zone create request: %w", err)
 519  	}
 520  
 521  	zoneMap := filterZoneCreate(params.CreateZone)
 522  	var zoneResponse ZoneResponse
 523  	resp, err := d.Exec(req, &zoneResponse, zoneMap)
 524  	if err != nil {
 525  		return fmt.Errorf("create zone request failed: %w", err)
 526  	}
 527  	defer session.CloseResponseBody(resp)
 528  
 529  	if resp.StatusCode != http.StatusCreated {
 530  		return d.Error(resp)
 531  	}
 532  
 533  	if strings.ToUpper(params.CreateZone.Type) == "PRIMARY" {
 534  		// Timing issue with Create immediately followed by SaveChangelist
 535  		for _, shouldClear := range params.ClearConn {
 536  			// should only be one entry
 537  			if shouldClear {
 538  				logger.Info("Clearing Idle Connections")
 539  				d.Client().CloseIdleConnections()
 540  			}
 541  		}
 542  	}
 543  
 544  	return nil
 545  }
 546  
 547  func (d *dns) SaveChangeList(ctx context.Context, params SaveChangeListRequest) error {
 548  	// This lock will restrict the concurrency of API calls
 549  	// to 1 save request at a time. This is needed for the Soa.Serial value which
 550  	// is required to be incremented for every subsequent update to a zone
 551  	// so we have to save just one request at a time to ensure this is always
 552  	// incremented properly
 553  
 554  	zoneWriteLock.Lock()
 555  	defer zoneWriteLock.Unlock()
 556  
 557  	logger := d.Log(ctx)
 558  	logger.Debug("SaveChangeList")
 559  
 560  	if err := params.Validate(); err != nil {
 561  		return fmt.Errorf("%s: %w: %s", ErrSaveChangeList, ErrStructValidation, err)
 562  	}
 563  
 564  	reqBody, err := convertStructToReqBody("")
 565  	if err != nil {
 566  		return fmt.Errorf("failed to generate request body: %w", err)
 567  	}
 568  
 569  	postURL := fmt.Sprintf("/config-dns/v2/changelists?zone=%s", params.Zone)
 570  	req, err := http.NewRequestWithContext(ctx, http.MethodPost, postURL, reqBody)
 571  	if err != nil {
 572  		return fmt.Errorf("failed to create SaveChangeList request: %w", err)
 573  	}
 574  
 575  	resp, err := d.Exec(req, nil)
 576  	if err != nil {
 577  		return fmt.Errorf("SaveChangeList request failed: %w", err)
 578  	}
 579  	defer session.CloseResponseBody(resp)
 580  
 581  	if resp.StatusCode != http.StatusCreated {
 582  		return d.Error(resp)
 583  	}
 584  
 585  	return nil
 586  }
 587  
 588  func (d *dns) SubmitChangeList(ctx context.Context, params SubmitChangeListRequest) error {
 589  	// This lock will restrict the concurrency of API calls
 590  	// to 1 save request at a time. This is needed for the Soa.Serial value which
 591  	// is required to be incremented for every subsequent update to a zone
 592  	// so we have to save just one request at a time to ensure this is always
 593  	// incremented properly
 594  
 595  	zoneWriteLock.Lock()
 596  	defer zoneWriteLock.Unlock()
 597  
 598  	logger := d.Log(ctx)
 599  	logger.Debug("SubmitChangeList")
 600  
 601  	if err := params.Validate(); err != nil {
 602  		return fmt.Errorf("%s: %w: %s", ErrSubmitChangeList, ErrStructValidation, err)
 603  	}
 604  
 605  	reqBody, err := convertStructToReqBody("")
 606  	if err != nil {
 607  		return fmt.Errorf("failed to generate request body: %w", err)
 608  	}
 609  
 610  	postURL := fmt.Sprintf("/config-dns/v2/changelists/%s/submit", params.Zone)
 611  	req, err := http.NewRequestWithContext(ctx, http.MethodPost, postURL, reqBody)
 612  	if err != nil {
 613  		return fmt.Errorf("failed to create SubmitChangeList request: %w", err)
 614  	}
 615  
 616  	resp, err := d.Exec(req, nil)
 617  	if err != nil {
 618  		return fmt.Errorf("SubmitChangeList request failed: %w", err)
 619  	}
 620  	defer session.CloseResponseBody(resp)
 621  
 622  	if resp.StatusCode != http.StatusNoContent {
 623  		return d.Error(resp)
 624  	}
 625  
 626  	return nil
 627  }
 628  
 629  func (d *dns) UpdateZone(ctx context.Context, params UpdateZoneRequest) error {
 630  	// This lock will restrict the concurrency of API calls
 631  	// to 1 save request at a time. This is needed for the Soa.Serial value which
 632  	// is required to be incremented for every subsequent update to a zone
 633  	// so we have to save just one request at a time to ensure this is always
 634  	// incremented properly
 635  
 636  	zoneWriteLock.Lock()
 637  	defer zoneWriteLock.Unlock()
 638  
 639  	logger := d.Log(ctx)
 640  	logger.Debug("Zone Update")
 641  
 642  	if err := ValidateZone(params.CreateZone); err != nil {
 643  		return err
 644  	}
 645  
 646  	zoneMap := filterZoneCreate(params.CreateZone)
 647  
 648  	putURL := fmt.Sprintf("/config-dns/v2/zones/%s", params.CreateZone.Zone)
 649  	req, err := http.NewRequestWithContext(ctx, http.MethodPut, putURL, nil)
 650  	if err != nil {
 651  		return fmt.Errorf("failed to create Get Update request: %w", err)
 652  	}
 653  
 654  	var result ZoneResponse
 655  	resp, err := d.Exec(req, &result, zoneMap)
 656  	if err != nil {
 657  		return fmt.Errorf("zone update request failed: %w", err)
 658  	}
 659  	defer session.CloseResponseBody(resp)
 660  
 661  	if resp.StatusCode != http.StatusOK {
 662  		return d.Error(resp)
 663  	}
 664  
 665  	return nil
 666  }
 667  
 668  func filterZoneCreate(zone *ZoneCreate) map[string]interface{} {
 669  	zoneType := strings.ToUpper(zone.Type)
 670  	filteredZone := make(map[string]interface{})
 671  	zoneElems := reflect.ValueOf(zone).Elem()
 672  	for i := 0; i < zoneElems.NumField(); i++ {
 673  		varName := zoneElems.Type().Field(i).Name
 674  		varLower := zoneStructMap[varName]
 675  		varValue := zoneElems.Field(i).Interface()
 676  		switch varName {
 677  		case "Target":
 678  			if zoneType == "ALIAS" {
 679  				filteredZone[varLower] = varValue
 680  			}
 681  		case "TSIGKey":
 682  			if zoneType == "SECONDARY" {
 683  				filteredZone[varLower] = varValue
 684  			}
 685  		case "Masters":
 686  			if zoneType == "SECONDARY" {
 687  				filteredZone[varLower] = varValue
 688  			}
 689  		case "SignAndServe":
 690  			if zoneType != "ALIAS" {
 691  				filteredZone[varLower] = varValue
 692  			}
 693  		case "SignAndServeAlgorithm":
 694  			if zoneType != "ALIAS" {
 695  				filteredZone[varLower] = varValue
 696  			}
 697  		case "OutboundZoneTransfer":
 698  			// this is workaround for the check if value is not nil to avoid adding empty entry
 699  			switch v := varValue.(type) {
 700  			case *OutboundZoneTransfer:
 701  				{
 702  					if v != nil {
 703  						filteredZone[varLower] = varValue
 704  					}
 705  				}
 706  			default:
 707  				filteredZone[varLower] = varValue
 708  			}
 709  		default:
 710  			filteredZone[varLower] = varValue
 711  		}
 712  	}
 713  
 714  	return filteredZone
 715  }
 716  
 717  // ValidateZone validates ZoneCreate Object
 718  func ValidateZone(zone *ZoneCreate) error {
 719  	if len(zone.Zone) == 0 {
 720  		return fmt.Errorf("Zone name is required")
 721  	}
 722  	zType := strings.ToUpper(zone.Type)
 723  	if zType != "PRIMARY" && zType != "SECONDARY" && zType != "ALIAS" {
 724  		return fmt.Errorf("Invalid zone type")
 725  	}
 726  	if zType != "SECONDARY" && zone.TSIGKey != nil {
 727  		return fmt.Errorf("TsigKey is invalid for %s zone type", zType)
 728  	}
 729  	if zType == "ALIAS" {
 730  		if len(zone.Target) == 0 {
 731  			return fmt.Errorf("Target is required for Alias zone type")
 732  		}
 733  		if len(zone.Masters) > 0 {
 734  			return fmt.Errorf("Masters is invalid for Alias zone type")
 735  		}
 736  		if zone.SignAndServe {
 737  			return fmt.Errorf("SignAndServe is invalid for Alias zone type")
 738  		}
 739  		if len(zone.SignAndServeAlgorithm) > 0 {
 740  			return fmt.Errorf("SignAndServeAlgorithm is invalid for Alias zone type")
 741  		}
 742  		return nil
 743  	}
 744  	// Primary or Secondary
 745  	if len(zone.Target) > 0 {
 746  		return fmt.Errorf("Target is invalid for %s zone type", zType)
 747  	}
 748  	if len(zone.Masters) > 0 && zType == "PRIMARY" {
 749  		return fmt.Errorf("Masters is invalid for Primary zone type")
 750  	}
 751  
 752  	return nil
 753  }
 754  
 755  func (d *dns) GetZoneNames(ctx context.Context, params GetZoneNamesRequest) (*GetZoneNamesResponse, error) {
 756  	logger := d.Log(ctx)
 757  	logger.Debug("GetZoneNames")
 758  
 759  	if err := params.Validate(); err != nil {
 760  		return nil, fmt.Errorf("%s: %w: %s", ErrGetZoneNames, ErrStructValidation, err)
 761  	}
 762  
 763  	getURL := fmt.Sprintf("/config-dns/v2/zones/%s/names", params.Zone)
 764  	req, err := http.NewRequestWithContext(ctx, http.MethodGet, getURL, nil)
 765  	if err != nil {
 766  		return nil, fmt.Errorf("failed to create GetZoneNames request: %w", err)
 767  	}
 768  
 769  	var result GetZoneNamesResponse
 770  	resp, err := d.Exec(req, &result)
 771  	if err != nil {
 772  		return nil, fmt.Errorf("GetZoneNames request failed: %w", err)
 773  	}
 774  	defer session.CloseResponseBody(resp)
 775  
 776  	if resp.StatusCode != http.StatusOK {
 777  		return nil, d.Error(resp)
 778  	}
 779  
 780  	return &result, nil
 781  }
 782  
 783  func (d *dns) GetZoneNameTypes(ctx context.Context, params GetZoneNameTypesRequest) (*GetZoneNameTypesResponse, error) {
 784  	logger := d.Log(ctx)
 785  	logger.Debug(" GetZoneNameTypes")
 786  
 787  	if err := params.Validate(); err != nil {
 788  		return nil, fmt.Errorf("%s: %w: %s", ErrGetZoneNameTypes, ErrStructValidation, err)
 789  	}
 790  
 791  	getURL := fmt.Sprintf("/config-dns/v2/zones/%s/names/%s/types", params.Zone, params.ZoneName)
 792  	req, err := http.NewRequestWithContext(ctx, http.MethodGet, getURL, nil)
 793  	if err != nil {
 794  		return nil, fmt.Errorf("failed to create GetZoneNameTypes request: %w", err)
 795  	}
 796  
 797  	var result GetZoneNameTypesResponse
 798  	resp, err := d.Exec(req, &result)
 799  	if err != nil {
 800  		return nil, fmt.Errorf("GetZoneNameTypes request failed: %w", err)
 801  	}
 802  	defer session.CloseResponseBody(resp)
 803  
 804  	if resp.StatusCode != http.StatusOK {
 805  		return nil, d.Error(resp)
 806  	}
 807  
 808  	return &result, nil
 809  }
 810  
 811  func (d *dns) GetZonesDNSSecStatus(ctx context.Context, params GetZonesDNSSecStatusRequest) (*GetZonesDNSSecStatusResponse, error) {
 812  	logger := d.Log(ctx)
 813  	logger.Debug("GetZonesDNSSecStatus")
 814  
 815  	if err := params.Validate(); err != nil {
 816  		return nil, fmt.Errorf("%w: %s", ErrStructValidation, err)
 817  	}
 818  
 819  	req, err := http.NewRequestWithContext(ctx, http.MethodPost, "/config-dns/v2/zones/dns-sec-status", nil)
 820  	if err != nil {
 821  		return nil, fmt.Errorf("failed to create GetZonesDNSSecStatus request: %w", err)
 822  	}
 823  
 824  	var result GetZonesDNSSecStatusResponse
 825  	resp, err := d.Exec(req, &result, params)
 826  	if err != nil {
 827  		return nil, fmt.Errorf("GetZonesDNSSecStatus request failed: %w", err)
 828  	}
 829  	defer session.CloseResponseBody(resp)
 830  
 831  	if resp.StatusCode != http.StatusOK {
 832  		return nil, d.Error(resp)
 833  	}
 834  
 835  	return &result, nil
 836  }
 837