A modern, idiomatic Go client for the Chef Infra / CINC Server API.
go get github.com/cinc-project/cinc-api
key, _ := cinc.LoadKeyFile("/etc/chef/client.pem")
c, err := cinc.NewClient(cinc.Config{
ServerURL: "https://chef.example.com",
Org: "myorg",
ClientName: "node1",
Key: key,
})
node, _, err := c.Nodes.Get(context.Background(), "web01")
Authentication uses the Chef v1.3 SHA-256 signed-header protocol. The following endpoint families are implemented:
| Service | Path | Methods |
|---|---|---|
c.ACLs |
/<object>/<name>/_acl |
GetTarget / SetTargetPermission / Grant / Revoke on an ACLTarget (ObjectACL, OrgACL, UserACL); Get / SetPermission, GetOrg / SetOrgPermission and GetUser / SetUserPermission |
c.Associations |
/organizations/O/users, /association_requests, /users/U/... |
Members (ListMembers/GetMember/AddMember/RemoveMember), org invites (ListInvites/Invite/RescindInvite), user invites (ListUserInvites/UserInviteCount/RespondInvite) and ListUserOrgs |
c.Clients |
/clients |
List / Get / Create / Update / Delete / Reregister |
c.Containers |
/containers |
List / Get / Create / Delete |
c.Cookbooks |
/cookbooks |
List (latest version of each) / ListVersions (every cookbook, num_versions) / GetVersions (one cookbook, num_versions) / Get (with metadata) / Delete / Upload (sandbox flow) / Download / DownloadFiles (from a fetched manifest) / ListLatest / ListRecipes |
c.CookbookArtifacts |
/cookbook_artifacts |
List / GetVersions (one artifact) / Get (with metadata) / Delete / Upload |
c.DataBags |
/data |
List / Create / Delete; per-bag Items handle for CRUD plus GetDecrypted / CreateEncrypted / UpdateEncrypted; DataBagItem.Encrypt/Decrypt/IsEncrypted for the Chef encrypted-data-bag format (writes v3 AES-256-GCM, reads v1/v2/v3) |
c.Cookbooks |
/cookbooks |
List / GetVersions (one cookbook, num_versions) / Get (with metadata) / Delete / Upload (sandbox flow) / Download / ListLatest / ListRecipes |
c.CookbookArtifacts |
/cookbook_artifacts |
List / GetVersions (one artifact) / Get (with metadata) / Delete / Upload; CookbookArtifactListEntry.Has(identifier) |
c.DataBags |
/data |
List / Create / Delete; per-bag Items handle for CRUD; DataBagItem.Encrypt/Decrypt/IsEncrypted for the Chef encrypted-data-bag format (writes v3 AES-256-GCM, reads v1/v2/v3) |
c.Environments |
/environments |
List / Get / Create / Update / Delete / ListCookbooks / GetCookbook / CookbookVersions / ListNodes / ListRecipes / RoleRunList |
c.Groups |
/groups |
List / Get / Create / Update / Delete / AddMembers / RemoveMembers |
c.Keys |
/users/U/keys, /clients/C/keys |
User(name) / Client(name) → List / Get / Create / Update / Delete |
c.License |
/license |
Get (node-license usage) |
c.Nodes |
/nodes |
List / Get / Create / Update / Modify / Delete |
c.Orgs |
/organizations (top-level) |
List / Get / Create / Update / Delete |
c.Policies |
/policies |
List / Get / Delete / GetRevision / CreateRevision / DeleteRevision / PushRevision |
c.PolicyGroups |
/policy_groups |
List / Get / Delete / GetPolicy / PutPolicy / DeletePolicy |
c.Principals |
/principals/<name> |
Get (public key(s) + type for a user/client) |
c.RequiredRecipe |
/required_recipe |
Get (returns Ruby text/plain) |
c.Roles |
/roles |
List / Get / Create / Update / Delete / Environments / EnvironmentRunList |
c.Search |
/search/INDEX |
Query (with WithStart/WithRows/WithPartial/WithPartialPaths), All (iter.Seq2 over rows, one page in memory at a time), Nodes (rows decoded as *Node), DataBagItems (rows unwrapped to DataBagItem), SearchAll, Indexes |
c.Stats |
/_stats (top-level, Basic auth) |
Get (Erchef/PostgreSQL/VM metrics; not Chef-signed) |
c.Status |
/_status |
Get (server health + keygen pool) |
c.Universe |
/universe (org + top-level) |
Get / GetGlobal (known cookbooks + dependencies) |
c.Users |
/users (top-level) |
List / Get / Create / Update / Delete / SetPassword / Authenticate |
Configurable via options: WithHTTPClient, WithUserAgent,
WithChefVersion, WithSkipTLSVerify, WithRootCAs, WithMaxRetries,
WithTransferTimeout. WithRootCAs(pool) verifies the server against a
private CA while keeping the default client and its 30-second timeout; with
WithHTTPClient it applies to a copy of that client's transport. Idempotent GETs are retried on 5xx and network errors,
but not on a failed TLS certificate check. Any other request is retried only
on 503 Service Unavailable, which means the server did not process it (a
Chef Server under load answers POST /users and POST /clients this way);
a Retry-After header on the 503 is honoured, up to 10 seconds. Signed
requests never follow redirects, since that would hand the signature to the
redirect target: a 3xx comes back as an *ErrorResponse naming the
Location. NewClient applies this to a copy of the http.Client, so one
passed with WithHTTPClient is left as it was. Cookbook file transfers to and from the pre-signed bookshelf URLs (the sandbox
PUTs of an upload, the GETs of a download) are retried the same way; the PUTs
are safe to repeat because they are addressed by content checksum. Those
transfers are bounded by WithTransferTimeout (default 10 minutes per
attempt, 0 for no limit beyond the context) instead of the http.Client
timeout, so a large file on a slow link is not cut off by the 30-second API
timeout.
Standalone helpers for working with Chef/CINC identities and the node object model, so callers don't re-encode server conventions:
ParseServerURL(raw)— splithttps://host/organizations/<org>into the base server URL and org (the inverse ofNewClient'sServerURL/Org).FormatServerURL(serverURL, org)— the inverse ofParseServerURL: join them back intohttps://host/organizations/<org>, escaping the org.Client.ServerURL()/Org()/ClientName()— the identity a client was built with, so callers need not keep their own copy.Response.ServerAPIVersion()— parse theX-Ops-Server-API-Versionheader every Chef Server response carries (supported min/max, and the requested and answered versions);Client.ServerAPIVersion(ctx)probesGET /server_api_versionfor the same when no other response is at hand, returningErrNoServerAPIVersion(never a version of 0) when the answer reports no range.ErrorResponse.ServerMessage()— just the server's error message(s), joined with;, withoutError()'scinc: METHOD PATH: CODE:prefix or the 401 hint (""when the server sent none).GenerateKeyPair()— mint a 2048-bit RSA key pair as PEM (the generation counterpart toParseKey/LoadKeyFile).Nodeaccessors —Tags/SetTags/AddTags/RemoveTags(stored atnormal.tags),AddRunListItems/RemoveRunListItems,Attribute/AttributeString/AttributeScalar(precedence-aware lookup, dotted paths; a one-element array reads as its element, and a map or null is not a scalar, soAttributeStringgives""andAttributeScalarreportsfalserather than Go'smap[...]text),LastCheckin()(fromautomatic.ohai_time), andEnvironmentName()(_defaultwhen unset).NormalizeRunListItem(item)/NormalizeRunList(items)— the run-list form erchef stores: a barenginxbecomesrecipe[nginx], then exact duplicates are dropped in order.NodeandRoleAddRunListItems/RemoveRunListItemscompare and write normalized entries, songinxandrecipe[nginx]are the same item.ValidateRunListItem(item)— erchef's run-list entry check (role[NAME], orCOOKBOOK[::RECIPE][@VERSION], bare or inrecipe[...]), wrappingErrInvalidRunListItem, so a malformed entry such asrecipe[is refused before the server answers the save with a 400.NormalizeRunListdoes not validate; call this on user input first.Nodes.Modify(name, fn)— read-modify-write: get the node, applyfn, and PUT it only if its encoding changed (a rename is refused). Nodes have no optimistic concurrency, so a concurrent write in between (such as a chef-client run) is overwritten.Clients.CreateandUsers.Createask the server to generate thedefaultkeypair (returned inChefKey.PrivateKey) unless aPublicKeyis set; under API v1 the server would otherwise create them without a key.KeyScope.Createdoes the same for an added key, and sends an emptyExpirationDateas"infinity", since the server requires one.Users.SetPassword(name, password)— change a password. The server's user PUT is a full update, so this re-sends the user's current fields with it.SuperuserName—"pivotal", the built-in superuser that creating orgs,/authenticate_userand invitation-free org membership are reserved to.Clients.Reregister(name)— regenerate a client'sdefaultkey and return the new private key (creating one if the client has none).ParsePolicyfileLock(data)/LoadPolicyfileLock(path)— parse aPolicyfile.lock.jsoninto aPolicyRevision.CompareCookbookVersions(a, b)orders cookbook versions the way Chef does (numericx.y[.z], so10.0.0is newer than9.0.0); anything that is not a valid version sorts below every valid one. Every version list the client returns (Cookbooks.List/ListVersions/GetVersions,Environments.ListCookbooks/GetCookbook) is sorted newest-first with it and trimmed tonum_versions, whatever the server sent, and an invalidnum_versionsis rejected before a request is made.LatestVersionis the_latestversion alias.Policyfile.lock.jsoninto aPolicyRevision. The policy name, revision id, cookbook lock names and identifiers are checked against the patterns Chef Server validates them with, so a returned name is safe to use in a path or generated config.CookbookLockaccessors —Origin()(classify a lock'ssource_optionsaspath/artifactserver/git/chef_serverand return its location),PinnedVersion()(thesource_optionsversion, falling back to the lock's top-level version),DottedIdentifier()(the dotted-decimal identifier, falling back to the identifier),GitRef()(the gitrevision, falling back toref,tag,branch) andGitSubdir()(the gitrel).DataBagItem.Encrypt(secret)/Decrypt(secret)/IsEncrypted()— the Chef encrypted-data-bag-item codec.Encryptboxes every value exceptidin a version-3 (AES-256-GCM) wrapper;Decryptreads versions 1, 2, and 3 and is byte-for-byte compatible with knife/chef-client. The AES key issha256(secret); values round-trip through Chef'sjson_wrapperboxing.Encryptrefuses an item that already holds an encrypted value (ErrAlreadyEncrypted) rather than encrypting the ciphertext again.Items(bag).GetDecrypted(id, secret)/CreateEncrypted(item, secret)/UpdateEncrypted(item, secret)— the encrypted read and write paths in one call each; an edit isGetDecrypted, a change, thenUpdateEncrypted.LoadDataBagSecret(path)/ParseDataBagSecret(data)— read a shared secret file exactly as Chef'sEncryptedDataBagItem.load_secretdoes: leading and trailing NUL and ASCII whitespace stripped, UTF-8 required (ErrInvalidDataBagSecret), an empty secret refused (ErrEmptyDataBagSecret). Chef's remote (URL) secrets are not supported.DataBagItem.Validate()— the non-empty stringidcheckCreate,UpdateandEncryptapply (ErrMissingDataBagItemID), for vetting an edited item up front.DataBagItem.Content()— the item withoutidand thechef_type/data_bagkeys a server adds to echoes and search rows.Policies.PushRevision(lockJSON, group, cookbooks)— the server-side half ofchef push: upload each pinned cookbook as an artifact under its lock name, then associate the revision with a policy group. The lock bytes are sent verbatim so no fields are lost. Artifacts the server already has are skipped, so the same lock can be pushed to several groups and a failed push can be retried. It returns aPushResult: theRevision, and the sorted lock names itUploadedand foundAlreadyPresent. A cookbook whose metadata names a different cookbook, or whose version differs from the lock's, is refused before anything is sent.LoadChefignore(dir)/Chefignore.Ignores(relPath)— Chef's chefignore handling: the nearestchefignoreindiror any parent (so a chef-repo'scookbooks/chefignoreapplies), with each pattern matched against the cookbook-relative path exactly as Ruby'sFile.fnmatch?does. Cookbook uploads, archives, and identifier computation agree on which files belong to a cookbook.LocalCookbookFromDirhonors it, and like Chef's loader also skips dot-directories at the cookbook root and takes the cookbook name frommetadata.json/metadata.rbrather than the directory name (which it falls back to, resolved first so.is named after the working directory).LoadCookbookMetadata(dir)/ParseMetadataJSON(data)/ParseMetadataRb(data)— read a cookbook's metadata into aCookbookMetadata, preferringmetadata.jsonovermetadata.rbas Chef's loader does.metadata.rbis Ruby and is never evaluated: only calls whose arguments are all literals are read (name,version,description,long_description,maintainer,maintainer_email,license,source_url,issues_url,depends,supports,provides,chef_version,ohai_version,gem,recipe,privacy,eager_load_libraries), normalized asChef::Cookbook::Metadatastores them (version '1.2'is1.2.0,depends 'apt', '1.2'is= 1.2), and a call Chef would raise on (a bad constraint, a self-dependency, a wrong argument type) is an error. Other calls are skipped, except a computedversion, which returnsErrMetadataVersionNotLiteralalongside the rest of the metadata rather than silently becoming 0.0.0.CookbookMetadata.CompiledJSON()— themetadata.jsonChef compiles (knife cookbook metadata, a Supermarket upload, achef export): every fieldChef::Cookbook::Metadata#to_hwrites, with Chef's defaults (license "All rights reserved",version "0.0.0",eager_load_libraries true, empty strings,{}and[]) for anything unset. A name is required.LocalCookbookFromDir(dir, version)— load a cookbook forCookbooks.Upload/CookbookArtifacts.Upload.LocalCookbook.Metadata(aCookbookMetadata: name, version, description, maintainer, license, dependencies, platforms, chef/ohai versions, …) is sent as the manifest'smetadatablock, which Chef Server requires and chef-client reads. It is filled byLoadCookbookMetadata, so anything computed inmetadata.rbneeds ametadata.jsonor an edit toMetadata. An emptyversionuses the metadata's version (an error ifmetadata.rbcomputes it); one that disagrees with it is an error.UnwrapSearchRow(row)— the object a search row describes: thedataof a partial-search row ({"url", "data"}), theraw_dataof theChef::DataBagItemenvelope a full data bag search wraps each item in, and any other row unchanged.WithPartialPaths("kernel.release", ...)builds a partial search from dotted paths, keyed by the path itself.Groups.AddMembers(group, kind, names...)/RemoveMembers(...)— change one kind of member (MemberUser,MemberClient,MemberGroup;ParseMemberKindreads"user"/"users"and so on) with a read, a PUT that is skipped when nothing changes, and a read-back. The returnedMemberChangesplits the names intoChanged,Unchanged(already as asked) andDropped: the server accepts a group PUT naming an actor that does not exist and silently leaves it out, so a successful PUT alone does not mean the member was added.Groupdecodes both of the server's shapes: members in top-levelusers/clients/groupsarrays (GET) or nested under anactorsobject (the PUT body, which erchef echoes back). The flatactorsarray a GET also carries is ignored, since the typed lists already hold its names. disagrees with it is an error.LocalCookbookFromDir(dir, version, SkipChefignore())keeps the files chefignore would drop (every other selection rule still applies), for packaging a cookbook as-is; chef-cli always applies chefignore, so such a cookbook'sIdentifiers()are not the ones aPolicyfile.lock.jsonshould carry.LocalCookbook.Files()/LocalCookbook.Identifiers()— the filesLocalCookbookFromDirselected (cookbook-relativePath,DiskPath, MD5Checksum), sorted by path, for callers that archive or copy a cookbook; and the Policyfile content identifier and dotted-decimal identifier over exactly those files, as chef-cli computes them forPolicyfile.lock.json(SHA-1 of the sortedpath:md5lines), so a lock always names the files an upload sends.ACL/ACEmerge helpers —ACL.ACEFor(perm)selects the ACE for one permission,ACE.AddMembers/RemoveMembersdedupe-add or remove actors and groups (reporting whether anything changed), andExpandPerm("all")expands to the five standard permissions — the reusable core of an ACL grant/revoke.- ACL targets and grant/revoke —
ObjectACL(cinc.ACLNodes, "web01"),OrgACL()andUserACL("alice")name the object whose ACL is read or written; theACL*constants (ACLDataBagsis"data", and so on, listed inACLObjectTypes) are the object types erchef serves_aclon.ACLs.Grant/Revoke(ctx, target, perm, actors, groups)take a permission or"all", read the ACL once, write only the permissions that change, and return those; a write that fails part way returns the permissions already changed and an*ACLChangeErrornaming the one that failed. A member the server cannot resolve is a 400,ErrBadRequest.
Licensed under the Apache License 2.0.