Search code examples
azurepowershellazure-cosmosdb

The partition key supplied in x-ms-partitionkey header has fewer components than defined in the collection


I am trying to send a request to the Cosmos DB API in Azure, using PowerShell, but I keep getting the following error back from the service:

"The partition key supplied in x-ms-partitionkey header has fewer components than defined in the collection."

This is also the case when I use the x-ms-documentdb-partitionkey header, and happens whether I attempt a GET (read) request, or try to POST/PUT (create/update) a document. If I try to send a value of an empty array, or an array with a single empty string, or the enumeration "None", I get an error message that "partition key None is invalid" (or whatever value I gave).

However, my collection was created before partition keys were required, so my collection originally did not have partition keys. After migrating my database, the collection Settings tab shows a "Partition key" value of /_partitionKey, with Large partition key being enabled. There is also a note that the container is non-hierarchically partitioned. BUT, none of my documents have a property "_partitionKey", and no value for one.

How can I work with the Cosmos API using native PowerShell?


Solution

  • After an enormous amount of searching and trial/error, I was finally able to get something to work. Feel free to use this for your own PowerShell needs.

    Authenticate

    First, you'll need to authenticate. You can hardcode the access key, or you can dynamically retrieve it using the following command.

    $keys = Invoke-AzResourceAction -Action "listkeys" `
        -ResourceType "Microsoft.DocumentDb/databaseAccounts" `
        -ResourceGroupName $rgName `
        -Name $cosmosAccountName `
        -ApiVersion "2015-04-08" `
        -Force
    
    $keys.primaryMasterKey            # Use this one for writing.
    $keys.primaryReadonlyMasterKey    # Use this one for reading.
    

    You'll need to take that key, and generate an authentication header. But to do that, you need to know what URL you are going to be making a request against, and what verb (e.g. POST, PUT, etc.).

    If you are executing a query, your ResourceLink would be the collection documents (e.g. dbs/myDatabase/colls/myCollection/docs). If you are creating or changing a document, your ResourceLink would be a specific document (e.g. dbs/myDatabase/colls/myCollection/docs/myDocGUID). In either of these examples, your ResourceType would be docs.

    # Credit from this URL: https://sql-articles.com/articles/azure/cosmos-db/provisioning-azure-cosmos-db-using-powershell/
    
    function Generate-MasterKeyAuthorizationSignature {
        [CmdletBinding()]
        [OutputType([string])]
        Param (
            # The HTTP verb going to be used with this authorization signature.  Can choose from 'DELETE', 'GET', 'PATCH', 'POST', or 'PUT'.
            [Parameter(Mandatory)]
            [ValidateSet("DELETE", "GET", "PATCH", "POST", "PUT")]
            [string] $Verb,
    
            # The access key to use for authentication to the target.
            [Parameter(Mandatory)]
            [string] $Key,
    
            # Optional Parameter.  The link of the resource we are requesting access to (e.g. dbs/myDatabase/colls/myCollection/docs/myDocID).
            [Parameter()]
            [string] $ResourceLink = [string]::Empty,
    
            # Optional Parameter.  The type of resource we are requesting access to (e.g. docs).
            [Parameter()]
            [string] $ResourceType = [string]::Empty,
    
            # Optional Parameter.  The date and time to attach to this signature.  Default value is right now, UTC.
            [Parameter()]
            [string] $DateTime = ([DateTime]::UtcNow.ToString("r")),
    
            # Optional Parameter.  The type of key the Key value represents.  Default value is 'master'.
            [Parameter()]
            [string] $KeyType = "master",
    
            # Optional Parameter.  The algorithm version that was used to generate the key (e.g. 1.0).  Default value is '1.0'.
            [Parameter()]
            [string] $TokenVersion = "1.0"
        )
    
        Process {
            $hmacSha256 = New-Object System.Security.Cryptography.HMACSHA256
            $hmacSha256.Key = [System.Convert]::FromBase64String($Key)
            if ($ResourceLink -eq $ResourceType) {
                $ResourceLink = [string]::Empty
            }
    
            $payload = "$($Verb.ToLowerInvariant())`n$($ResourceType.ToLowerInvariant())`n$ResourceLink`n$($DateTime.ToLowerInvariant())`n`n"
            $hashPayload = $hmacSha256.ComputeHash([System.Text.Encoding]::UTF8.GetBytes($payload))
            $signature = [System.Convert]::ToBase64String($hashPayload)
            return [System.Web.HttpUtility]::UrlEncode("type=$KeyType&ver=$TokenVersion&sig=$signature")
        }
    }
    

    If you declare and then invoke the above function, you'll get the value needed for your authorization header.

    Headers

    $headers = @{
        authorization         = $authSig
        "x-ms-date"           = $dateTime
        "x-ms-version"        = "2020-07-15"
        "x-ms-max-item-count" = 100
    }    # Shared headers used by all requests.
    

    If you are querying data, you'll need the following headers (in addition to those above):

    If you are changing data, you'll need the following headers (in addition to shared):

    • x-ms-documentdb-partitionkey: [] Your content type will be application/json. If you don't use partitions, or migrated from a partitionless collection, set an empty array [] as your partition key. Otherwise, give an array with a single string with your partition key (right now they only support using one at a time).

    Body

    If you are querying data, your body will be a JSON body containing the query. You can also play around with parameters if you need, as a separate JSON property.

    $body = @{
        query = "select * from C"
    }
    

    If you are changing data, your body will either be the entire document (when creating or replacing), OR a patch operation.

    $body = @{
        operations = @(
            @{
                op = "replace"
                path = "/myJsonPath"
                value = "blah"
            }
        )
    }
    

    Request

    Set your TLS to 1.2, just in case.

    [System.Net.ServicePointManager]::SecurityProtocol = [System.Net.SecurityProtocolType]::Tls12
    

    Finally, send your request. The content of a successful response will have a Documents array property that you can dig through.

    $response = Invoke-WebRequest -Method $verb -Uri $url -ContentType $contentType -Headers $headers -Body $body -UseBasicParsing
    

    It took me FOREVER to figure out the partition key stuff for collections that were created without a partition key, so I thought I would document it here. If you need more help, or want to dig more, check here:

    https://learn.microsoft.com/en-us/rest/api/cosmos-db/documents