Go Tableau Extract Refresh API exe help – Index out of Range Error

I have been trying to learn Go as a crash course. My task is to build a little program that takes in a token name, secret token, data source name, and parameter for checking on the current status of the tableau extract refresh. I am stuck on a parsing error that seems straightforward, but cannot find the simple error. Here is the code:

package main

import (
    "bytes"
    "encoding/json"
    "flag"
    "fmt"
    "io/ioutil"
    "net/http"
    "os"
    "strings"
    "time"
)

const URL = "https://xxx.xxxx.com/api/3.11/"

func main() {

    // get the login details from command line args
    tokenName := flag.String("token", "", "Personal Access Token Name")
    tokenSecret := flag.String("secret", "", "Personal Access Token Secret")
    datasourceName := flag.String("datasrc", "", "Datasource Name")
    refreshInterval := flag.Int("refresh", 5, "Refresh Interval in minutes")
    flag.Parse()

    // if login details are not provided, prompt for them
    if *tokenName == "" || *tokenSecret == "" {
        fmt.Println("Usage:")
        fmt.Println("  -token     : Personal Access Token Name")
        fmt.Println("  -secret : Personal Access Token Secret")
        fmt.Println("  -datasrc  : Datasource Name")
        fmt.Println("  -refresh  : Refresh Interval in minutes (Optional. Default is 5)")
        fmt.Println("n  Example:")
        fmt.Println("  " + os.Args[0] + " -token <Personal-Access-Token-Name> -secret <Personal-Access-Token-Secret> -siteid <server-admin-site-id>")
        return
    }

    // Sign in
    internalSiteID, authToken := signIn(*tokenName, *tokenSecret)
    fmt.Printf("internalSiteID: %s, authToken: %sn", internalSiteID, authToken)

    // Get datasource details
    datasrcID := getDatasrcDetails(internalSiteID, *datasourceName, authToken)
    fmt.Printf("datasrcID: %sn", datasrcID)

    // Run Refresh Extract
    jobID := runRefreshExtract(internalSiteID, authToken, datasrcID)

    // schedule a refresh status every 5 minutes
    ticker := time.NewTicker(time.Duration(*refreshInterval) * time.Minute)
    for range ticker.C {
        refreshJobStatus(internalSiteID, authToken, jobID)
    }
}

// signIn returns the internal site ID and auth token.
func signIn(tokenName, tokenSecret string) (internalSiteID string, authToken string) {

    // Sign In
    type (

        // Response body
        User struct {
            ID string `json:"id"`
        }

        Site struct {
            Id         string `json:"id"`
            ContentUrl string `json:"contentUrl"`
        }

        Credentials struct {
            Site        Site   `json:"site"`
            User        User   `json:"user"`
            NamedToken  string `json:"personalAccessTokenName"`
            SecretToken string `json:"personalAccessTokenSecret"`
            Token       string `json:"token"`
        }

        SignIn struct {
            Credentials Credentials `json:"credentials"`
        }

        // Request body
        SignInRequest struct {
            Credentials Credentials `json:"credentials"`
        }
    )

    // make sign in request
    signInRequest := SignInRequest{
        Credentials: Credentials{
            NamedToken:  tokenName,
            SecretToken: tokenSecret,
            Site: Site{
                ContentUrl: "", //trying to run without the site id
            },
        },
    }

    // generate request for sign-in as a JSON string
    signInData, err := json.Marshal(signInRequest)
    if err != nil {
        fmt.Printf("Error marshalling sign in request: %sn", err)
        return
    }

    // send request
    resp, err := http.Post(URL+"auth/signin",
        "application/json",
        bytes.NewBuffer(signInData),
    )
    if err != nil {
        fmt.Printf("Error marshalling sign in request: %sn", err)
        return
    }

    // read response
    defer resp.Body.Close()

    // parse response
    var signInResponse SignIn
    err = json.NewDecoder(resp.Body).Decode(&signInResponse)
    if err != nil {
        fmt.Printf("Error parsing sign in response: %sn", err)
        return
    }

    // return internal site ID and auth token
    internalSiteID = signInRequest.Credentials.Site.Id
    authToken = signInRequest.Credentials.Token
    return
}

// getDatasrcDetails returns the datasource details.
func getDatasrcDetails(internalSiteID, datasrcName, authToken string) (datasourceID string) {

    type (
        Pagination struct {
            PageNumber     string `json:"pageNumber"`
            PageSize       string `json:"pageSize"`
            TotalAvailable string `json:"totalAvailable"`
        }
        Project struct {
            ID   string `json:"id"`
            Name string `json:"name"`
        }
        Owner struct {
            ID string `json:"id"`
        }
        Tags struct {
        }
        Datasource struct {
            Project         Project   `json:"project"`
            Owner           Owner     `json:"owner"`
            Tags            Tags      `json:"tags"`
            ContentURL      string    `json:"contentUrl"`
            CreatedAt       time.Time `json:"createdAt"`
            EncryptExtracts string    `json:"encryptExtracts"`
            ID              string    `json:"id"`
            IsCertified     bool      `json:"isCertified"`
            Name            string    `json:"name"`
            Type            string    `json:"type"`
            UpdatedAt       time.Time `json:"updatedAt"`
            WebpageURL      string    `json:"webpageUrl"`
        }

        Datasources struct {
            Datasource []Datasource `json:"datasource"`
        }

        Response struct {
            Pagination  Pagination  `json:"pagination"`
            Datasources Datasources `json:"datasources"`
        }
    )

    req, err := http.NewRequest("GET", URL+"sites/"+internalSiteID+"/datasources?filter=name:eq:"+datasrcName, nil)
    if err != nil {
        fmt.Printf("Error parsing sign in response: %sn", err)
        return
    }
    req.Header.Set("X-Tableau-Auth", authToken)
    req.Header.Set("Accept", "application/json")

    resp, err := http.DefaultClient.Do(req)
    if err != nil {
        fmt.Printf("Error parsing sign in response: %sn", err)
        return
    }
    defer resp.Body.Close()

    // parse response
    var response Response
    err = json.NewDecoder(resp.Body).Decode(&response)
    if err != nil {
        fmt.Printf("Error parsing sign in response: %sn", err)
        return
    }

    // return datasource ID
    datasourceID = response.Datasources.Datasource[0].ID
    return
}

// runRefreshExtract runs a refresh extract request.
func runRefreshExtract(internalSiteID, authToken, datasrcID string) (jobID string) {

    type (
        Datasource struct {
            ID   string `json:"id"`
            Name string `json:"name"`
        }
        ExtractRefreshJob struct {
            Notes      string     `json:"notes"`
            Datasource Datasource `json:"datasource"`
        }
        Job struct {
            ExtractRefreshJob ExtractRefreshJob `json:"extractRefreshJob"`
            ID                string            `json:"id"`
            Mode              string            `json:"mode"`
            Type              string            `json:"type"`
            Progress          string            `json:"progress"`
            CreatedAt         time.Time         `json:"createdAt"`
            StartedAt         time.Time         `json:"startedAt"`
            CompletedAt       time.Time         `json:"completedAt"`
            FinishCode        string            `json:"finishCode"`
        }
        Response struct {
            Job Job `json:"job"`
        }
    )

    body := strings.NewReader(`<tsRequest></tsRequest>`)
    req, err := http.NewRequest("POST", URL+"sites/"+internalSiteID+"/datasources/"+datasrcID+"/refresh", body)
    if err != nil {
        fmt.Printf("Error parsing sign in response: %sn", err)
        return
    }
    req.Header.Set("X-Tableau-Auth", authToken)
    req.Header.Set("Accept", "application/json")
    req.Header.Set("Content-Type", "application/xml")

    resp, err := http.DefaultClient.Do(req)
    if err != nil {
        fmt.Printf("Error parsing sign in response: %sn", err)
        return
    }
    defer resp.Body.Close()

    // parse response
    var response Response
    err = json.NewDecoder(resp.Body).Decode(&response)
    if err != nil {
        fmt.Printf("Error parsing sign in response: %sn", err)
        return
    }

    // return job ID
    jobID = response.Job.ID
    return
}

// refreshJobStatus checks the status of a refresh job.
func refreshJobStatus(internalSiteID, authToken, jobID string) {

    req, err := http.NewRequest("GET", URL+"sites/"+internalSiteID+"/jobs/"+jobID, nil)
    if err != nil {
        fmt.Printf("Error parsing sign in response: %sn", err)
        return
    }
    req.Header.Set("X-Tableau-Auth", authToken)
    req.Header.Set("Accept", "application/json")

    resp, err := http.DefaultClient.Do(req)
    if err != nil {
        fmt.Printf("Error parsing sign in response: %sn", err)
        return
    }
    defer resp.Body.Close()

    // print response body
    body, err := ioutil.ReadAll(resp.Body)
    if err != nil {
        fmt.Printf("Error parsing sign in response: %sn", err)
        return
    }
    fmt.Println("-- refresh job status: ", resp.Status, string(body), "--")
}

the error I am getting seems obvious, but I swear my code follows the API spec in terms of what gets returned in the sign in response:

C:UsersxxxOneDriveDocumentsapiRefresh>main3.2 -token xxxx -secret xxxxx -datasrc xxxxx
Error parsing sign in response: invalid character '<' looking for beginning of value
internalSiteID: , authToken:
panic: runtime error: index out of range [0] with length 0

goroutine 1 [running]:
main.getDatasrcDetails({0x0?, 0xc000006018?}, {0xc00000a140?, 0x22?}, {0x0, 0x0})
        C:/Users/xxxxx/OneDrive/Documents/apiRefresh/main3.2.go:202 +0x516
main.main()
        C:/Users/xxxxxxxx/OneDrive/Documents/apiRefresh/main3.2.go:43 +0x23a

Can anyone help me figure out where I have gone wrong?

Leave a Comment