diff --git a/api/spec/json/agents.json b/api/spec/json/agents.json index b772b2a02..dea32063e 100644 --- a/api/spec/json/agents.json +++ b/api/spec/json/agents.json @@ -1,11 +1,11 @@ { - "information": { + "group": { "name": "agents", "description": "Cumulocity agents", "descriptionLong": "REST endpoint to interact with Cumulocity agents", "link": "https://cumulocity.com/guides/reference/inventory/" }, - "endpoints": [ + "commands": [ { "name": "listAgents", "description": "Get agent collection", @@ -155,7 +155,7 @@ }, { "name": "group", - "type": "[]devicegroup", + "type": "devicegroup[]", "description": "Filter by group inclusion", "format": "bygroupid(%s)" } @@ -232,7 +232,7 @@ "pathParameters": [ { "name": "id", - "type": "[]agent", + "type": "agent[]", "pipeline": true, "required": true, "description": "Agent ID" @@ -320,7 +320,7 @@ "pathParameters": [ { "name": "id", - "type": "[]agent", + "type": "agent[]", "pipeline": true, "required": true, "description": "Agent ID" @@ -404,7 +404,7 @@ "pathParameters": [ { "name": "id", - "type": "[]agent", + "type": "agent[]", "pipeline": true, "required": true, "description": "Agent ID" diff --git a/api/spec/json/alarms.json b/api/spec/json/alarms.json index db57a2486..3d9f2c1ce 100644 --- a/api/spec/json/alarms.json +++ b/api/spec/json/alarms.json @@ -1,11 +1,11 @@ { - "information": { + "group": { "name": "alarms", "description": "Cumulocity alarms", "descriptionLong": "REST endpoint to interact with Cumulocity alarms", "link": "https://cumulocity.com/guides/reference/alarms/" }, - "endpoints": [ + "commands": [ { "name": "getAlarmCollection", "method": "GET", @@ -66,7 +66,7 @@ "queryParameters": [ { "name": "device", - "type": "[]device", + "type": "device[]", "pipeline": true, "property": "source", "description": "Source device id." @@ -88,7 +88,7 @@ }, { "name": "status", - "type": "[]stringcsv", + "type": "stringcsv[]", "description": "Comma separated alarm statuses, for example ACTIVE,CLEARED.", "validationSet": [ "ACTIVE", @@ -188,7 +188,7 @@ "body": [ { "name": "device", - "type": "[]device", + "type": "device[]", "required": false, "pipeline": true, "property": "source.id", @@ -339,7 +339,7 @@ "queryParameters": [ { "name": "device", - "type": "[]device", + "type": "device[]", "property": "source", "pipeline": true, "description": "The ManagedObject that the alarm originated from" @@ -410,7 +410,7 @@ "pathParameters": [ { "name": "id", - "type": "[]id", + "type": "id[]", "pipeline": true, "required": true, "description": "Alarm id" @@ -514,7 +514,7 @@ "pathParameters": [ { "name": "id", - "type": "[]id", + "type": "id[]", "pipeline": true, "description": "Alarm id", "required": true @@ -579,7 +579,7 @@ "queryParameters": [ { "name": "device", - "type": "[]device", + "type": "device[]", "property": "source", "pipeline": true, "description": "Source device id." @@ -611,7 +611,7 @@ }, { "name": "status", - "type": "[]stringcsv", + "type": "stringcsv[]", "description": "Comma separated alarm statuses, for example ACTIVE,CLEARED.", "validationSet": [ "ACTIVE", @@ -720,7 +720,7 @@ "queryParameters": [ { "name": "device", - "type": "[]device", + "type": "device[]", "pipeline": true, "property": "source", "description": "Source device id." @@ -742,7 +742,7 @@ }, { "name": "status", - "type": "[]stringcsv", + "type": "stringcsv[]", "description": "Comma separated alarm statuses, for example ACTIVE,CLEARED.", "validationSet": [ "ACTIVE", diff --git a/api/spec/json/applications.json b/api/spec/json/applications.json index 7cced7a9a..e27ca92d5 100644 --- a/api/spec/json/applications.json +++ b/api/spec/json/applications.json @@ -1,11 +1,11 @@ { - "information": { + "group": { "name": "applications", "description": "Cumulocity applications", "descriptionLong": "REST endpoint to interact with Cumulocity applications", "link": "https://cumulocity.com/guides/reference/applications/" }, - "endpoints": [ + "commands": [ { "name": "getApplicationCollection", "description": "Get application collection", @@ -76,7 +76,7 @@ }, { "name": "user", - "type": "[]user", + "type": "user[]", "description": "The ID of a user that has access to the applications." } ] @@ -624,7 +624,7 @@ { "name": "binaryId", "alias": "id", - "type": "[]id", + "type": "id[]", "pipeline": true, "required": true, "description": "Application binary id" diff --git a/api/spec/json/auditRecords.json b/api/spec/json/auditRecords.json index fa2cdea0d..8f40526b4 100644 --- a/api/spec/json/auditRecords.json +++ b/api/spec/json/auditRecords.json @@ -1,11 +1,11 @@ { - "information": { + "group": { "name": "auditRecords", "description": "Cumulocity auditRecords", "descriptionLong": "REST endpoint to interact with Cumulocity auditRecords", "link": "https://cumulocity.com/guides/reference/auditing/#audit-api" }, - "endpoints": [ + "commands": [ { "name": "newAudit", "description": "Create audit record", @@ -275,7 +275,7 @@ "pathParameters": [ { "name": "id", - "type": "[]id", + "type": "id[]", "required": true, "pipeline": true, "description": "Audit id" diff --git a/api/spec/json/binaries.json b/api/spec/json/binaries.json index efaec1e04..9fc7cb6af 100644 --- a/api/spec/json/binaries.json +++ b/api/spec/json/binaries.json @@ -1,11 +1,11 @@ { - "information": { + "group": { "name": "binaries", "description": "Cumulocity binaries", "descriptionLong": "REST endpoint to interact with Cumulocity binaries", "link": "https://cumulocity.com/guides/reference/binaries/" }, - "endpoints": [ + "commands": [ { "name": "getBinaryCollection", "description": "Get binary collection", @@ -47,7 +47,7 @@ "queryParameters": [ { "name": "ids", - "type": "[]stringcsv", + "type": "stringcsv[]", "property": "ids", "description": "The managed object IDs to search for." }, @@ -83,7 +83,7 @@ }, { "name": "childDeviceId", - "type": "[]device", + "type": "device[]", "description": "Search for a specific child device and list all the groups to which it belongs." } ] @@ -140,7 +140,7 @@ "pathParameters": [ { "name": "id", - "type": "[]id", + "type": "id[]", "required": true, "pipeline": true, "description": "Inventory binary id" @@ -268,7 +268,7 @@ "pathParameters": [ { "name": "id", - "type": "[]id", + "type": "id[]", "required": true, "pipeline": true, "description": "Inventory binary id" @@ -325,7 +325,7 @@ "pathParameters": [ { "name": "id", - "type": "[]id", + "type": "id[]", "required": true, "pipeline": true, "description": "Inventory binary id" diff --git a/api/spec/json/bulkOperations.json b/api/spec/json/bulkOperations.json index 52b52ea3f..8b42f8824 100644 --- a/api/spec/json/bulkOperations.json +++ b/api/spec/json/bulkOperations.json @@ -1,11 +1,11 @@ { - "information": { + "group": { "name": "bulkOperations", "description": "Cumulocity bulk operations", "descriptionLong": "REST endpoint to interact with Cumulocity bulk operations", "link": "https://cumulocity.com/guides/reference/device-control/#bulk-operation-collection" }, - "endpoints": [ + "commands": [ { "name": "getBulkOperationCollection", "method": "GET", @@ -88,7 +88,7 @@ { "name": "status", "property": "generalStatus", - "type": "[]string", + "type": "string[]", "description": "Operation status, can be one of SUCCESSFUL, FAILED, EXECUTING or PENDING.", "validationSet": [ "CANCELED", @@ -136,7 +136,7 @@ "pathParameters": [ { "name": "id", - "type": "[]id", + "type": "id[]", "required": true, "pipeline": true, "description": "Bulk Operation id" @@ -177,7 +177,7 @@ "pathParameters": [ { "name": "id", - "type": "[]id", + "type": "id[]", "required": true, "pipeline": true, "description": "Bulk Operation id" @@ -234,7 +234,7 @@ "body": [ { "name": "group", - "type": "[]devicegroup", + "type": "devicegroup[]", "property": "groupId", "required": false, "pipeline": true, @@ -334,7 +334,7 @@ "pathParameters": [ { "name": "id", - "type": "[]id", + "type": "id[]", "description": "Bulk Operation id", "pipeline": true, "required": true @@ -391,7 +391,7 @@ "queryParameters": [ { "name": "id", - "type": "[]id", + "type": "id[]", "property": "bulkOperationId", "pipeline": true, "required": true, diff --git a/api/spec/json/configuration.json b/api/spec/json/configuration.json index c1325a21c..357860387 100644 --- a/api/spec/json/configuration.json +++ b/api/spec/json/configuration.json @@ -1,11 +1,11 @@ { - "information": { + "group": { "name": "configuration", "description": "Cumulocity configuration repository management", "descriptionLong": "Configuration management to create/list/delete configurations", "link": "https://cumulocity.com/guides/users-guide/device-management/#configuration-repository" }, - "endpoints": [ + "commands": [ { "name": "getConfigurationCollection", "method": "GET", @@ -269,7 +269,7 @@ "pathParameters": [ { "name": "id", - "type": "[]configuration", + "type": "configuration[]", "pipeline": true, "required": true, "description": "Configuration package (managedObject) id" @@ -392,7 +392,7 @@ "pathParameters": [ { "name": "id", - "type": "[]configuration", + "type": "configuration[]", "required": true, "pipeline": true, "description": "Configuration package (managedObject) id" @@ -460,7 +460,7 @@ "pathParameters": [ { "name": "id", - "type": "[]configuration", + "type": "configuration[]", "pipeline": true, "required": true, "description": "Configuration file (managedObject) id" @@ -524,7 +524,7 @@ "body": [ { "name": "device", - "type": "[]device", + "type": "device[]", "property": "deviceId", "required": false, "pipeline": true, @@ -552,7 +552,7 @@ }, { "name": "configuration", - "type": "[]configuration", + "type": "configuration[]", "property": "__tmp_configuration", "required": false, "description": "Configuration name or id" diff --git a/api/spec/json/currentApplication.json b/api/spec/json/currentApplication.json index c2e088589..8939ac602 100644 --- a/api/spec/json/currentApplication.json +++ b/api/spec/json/currentApplication.json @@ -1,11 +1,11 @@ { - "information": { + "group": { "name": "currentApplication", "description": "Cumulocity currentApplication", "descriptionLong": "REST endpoint to interact with Cumulocity currentApplication", "link": "https://cumulocity.com/guides/reference/applications/#current-application" }, - "endpoints": [ + "commands": [ { "name": "getCurrentApplication", "description": "Get current application", diff --git a/api/spec/json/currentTenant.json b/api/spec/json/currentTenant.json index f99e09ebb..61e4cff04 100644 --- a/api/spec/json/currentTenant.json +++ b/api/spec/json/currentTenant.json @@ -1,11 +1,11 @@ { - "information": { + "group": { "name": "currenttenant", "description": "Cumulocity current tenant", "descriptionLong": "Cumulocity current tenant commands", "link": "https://cumulocity.com/guides/reference/tenants/#tenants" }, - "endpoints": [ + "commands": [ { "name": "currentTenant", "description": "Get current tenant", diff --git a/api/spec/json/currentUser.json b/api/spec/json/currentUser.json index 768cac6e2..9bce26a1d 100644 --- a/api/spec/json/currentUser.json +++ b/api/spec/json/currentUser.json @@ -1,11 +1,11 @@ { - "information": { + "group": { "name": "currentuser", "description": "Cumulocity current user", "descriptionLong": "REST endpoint to interact with the current Cumulocity user", "link": "https://cumulocity.com/guides/reference/users/#user" }, - "endpoints": [ + "commands": [ { "name": "getCurrentUser", "description": "Get current user", diff --git a/api/spec/json/databroker.json b/api/spec/json/databroker.json index 9909b902b..d7fd50dd8 100644 --- a/api/spec/json/databroker.json +++ b/api/spec/json/databroker.json @@ -1,11 +1,11 @@ { - "information": { + "group": { "name": "databroker", "description": "Cumulocity databroker", "descriptionLong": "REST endpoint to interact with Cumulocity databroker", "link": "https://cumulocity.com/guides/users-guide/enterprise-tenant/#data-broker" }, - "endpoints": [ + "commands": [ { "name": "getDataBrokerConnectorCollection", "description": "Get data broker collection", @@ -64,7 +64,7 @@ "pathParameters": [ { "name": "id", - "type": "[]id", + "type": "id[]", "required": true, "pipeline": true, "description": "Data broker connector id" @@ -100,7 +100,7 @@ "pathParameters": [ { "name": "id", - "type": "[]id", + "type": "id[]", "required": true, "pipeline": true, "description": "Data broker connector id" diff --git a/api/spec/json/datahub.json b/api/spec/json/datahub.json index f46d3e351..62c49ee94 100644 --- a/api/spec/json/datahub.json +++ b/api/spec/json/datahub.json @@ -1,11 +1,11 @@ { - "information": { + "group": { "name": "datahub", "description": "Cumulocity IoT Data Hub api", "descriptionLong": "Data Hub api", "link": "https://cumulocity.com/guides/datahub/datahub-overview/" }, - "endpoints": [ + "commands": [ { "name": "query", "method": "POST", diff --git a/api/spec/json/datahubConfiguration.json b/api/spec/json/datahubConfiguration.json index f26b506fc..c337385a8 100644 --- a/api/spec/json/datahubConfiguration.json +++ b/api/spec/json/datahubConfiguration.json @@ -1,12 +1,12 @@ { - "information": { + "group": { "name": "datahub/configuration", "description": "Cumulocity IoT DataHub Configurations", "descriptionLong": "Cumulocity IoT DataHub Configurations", "link": "https://cumulocity.com/guides/datahub/datahub-overview/", "skip": true }, - "endpoints": [ + "commands": [ { "name": "list", "method": "GET", diff --git a/api/spec/json/datahubJobs.json b/api/spec/json/datahubJobs.json index 8692eac24..71e7bbe6c 100644 --- a/api/spec/json/datahubJobs.json +++ b/api/spec/json/datahubJobs.json @@ -1,11 +1,11 @@ { - "information": { + "group": { "name": "datahub/jobs", "description": "Cumulocity IoT DataHub Jobs", "descriptionLong": "Cumulocity IoT DataHub Jobs", "link": "https://cumulocity.com/guides/datahub/datahub-overview/" }, - "endpoints": [ + "commands": [ { "name": "createJob", "method": "POST", @@ -63,7 +63,7 @@ }, { "name": "context", - "type": "[]string", + "type": "string[]", "required": false, "description": "The context in which the query is executed" }, diff --git a/api/spec/json/datahubScheduler.json b/api/spec/json/datahubScheduler.json index 0f2c5eea2..8448295e6 100644 --- a/api/spec/json/datahubScheduler.json +++ b/api/spec/json/datahubScheduler.json @@ -1,12 +1,12 @@ { - "information": { + "group": { "name": "datahub/scheduler", "description": "Cumulocity IoT DataHub Scheduler", "descriptionLong": "Cumulocity IoT DataHub Scheduler", "link": "https://cumulocity.com/guides/datahub/datahub-overview/", "skip": true }, - "endpoints": [ + "commands": [ { "name": "list", "method": "GET", diff --git a/api/spec/json/datahubTenant.json b/api/spec/json/datahubTenant.json index becd5bd95..585ebc26f 100644 --- a/api/spec/json/datahubTenant.json +++ b/api/spec/json/datahubTenant.json @@ -1,12 +1,12 @@ { - "information": { + "group": { "name": "datahub/tenant", "description": "Cumulocity IoT DataHub Tenant information", "descriptionLong": "Cumulocity IoT DataHub Tenant information", "link": "https://cumulocity.com/guides/datahub/datahub-overview/", "skip": true }, - "endpoints": [ + "commands": [ { "name": "get", "method": "GET", diff --git a/api/spec/json/datahubUsers.json b/api/spec/json/datahubUsers.json index e74d00f0b..fe6587f6e 100644 --- a/api/spec/json/datahubUsers.json +++ b/api/spec/json/datahubUsers.json @@ -1,12 +1,12 @@ { - "information": { + "group": { "name": "datahub/users", "description": "Cumulocity IoT DataHub Users", "descriptionLong": "Cumulocity IoT DataHub Users", "link": "https://cumulocity.com/guides/datahub/datahub-overview/", "skip": true }, - "endpoints": [ + "commands": [ { "name": "list", "method": "GET", diff --git a/api/spec/json/deviceAvailability.json b/api/spec/json/deviceAvailability.json index 6e837fc73..ecca0d7a1 100644 --- a/api/spec/json/deviceAvailability.json +++ b/api/spec/json/deviceAvailability.json @@ -1,11 +1,11 @@ { - "information": { + "group": { "name": "devices/availability", "description": "Cumulocity device availability", "descriptionLong": "REST endpoint to interact with Cumulocity devices", "link": "https://cumulocity.com/guides/reference/inventory/" }, - "endpoints": [ + "commands": [ { "name": "setDeviceRequiredAvailability", "description": "Set required availability", @@ -66,7 +66,7 @@ "pathParameters": [ { "name": "id", - "type": "[]device", + "type": "device[]", "pipeline": true, "required": true, "description": "Device ID" @@ -139,7 +139,7 @@ "pathParameters": [ { "name": "id", - "type": "[]device", + "type": "device[]", "pipeline": true, "required": true, "description": "Device." diff --git a/api/spec/json/deviceCredentials.json b/api/spec/json/deviceCredentials.json index f1820786e..fec21156d 100644 --- a/api/spec/json/deviceCredentials.json +++ b/api/spec/json/deviceCredentials.json @@ -1,11 +1,11 @@ { - "information": { + "group": { "name": "deviceregistration", "description": "Cumulocity device credentials", "descriptionLong": "REST endpoint to interact with Cumulocity device credentials api", "link": "https://cumulocity.com/guides/reference/device-credentials/" }, - "endpoints": [ + "commands": [ { "name": "getNewDeviceRequestCollection", "method": "GET", @@ -75,7 +75,7 @@ "pathParameters": [ { "name": "id", - "type": "[]devicerequest", + "type": "devicerequest[]", "required": true, "pipeline": true, "description": "New Device Request ID" @@ -113,7 +113,7 @@ "body": [ { "name": "id", - "type": "[]id", + "type": "id[]", "required": true, "pipeline": true, "description": "Device identifier. Max: 1000 characters. E.g. IMEI" @@ -156,7 +156,7 @@ "pathParameters": [ { "name": "id", - "type": "[]devicerequest", + "type": "devicerequest[]", "required": true, "pipeline": true, "description": "Device identifier" @@ -214,7 +214,7 @@ "pathParameters": [ { "name": "id", - "type": "[]devicerequest", + "type": "devicerequest[]", "required": true, "pipeline": true, "description": "New Device Request ID" @@ -253,7 +253,7 @@ "body": [ { "name": "id", - "type": "[]devicerequest", + "type": "devicerequest[]", "required": true, "pipeline": true, "description": "Device identifier. Max: 1000 characters. E.g. IMEI" diff --git a/api/spec/json/deviceGroups.json b/api/spec/json/deviceGroups.json index a1cb8347b..9315c5a3a 100644 --- a/api/spec/json/deviceGroups.json +++ b/api/spec/json/deviceGroups.json @@ -1,11 +1,11 @@ { - "information": { + "group": { "name": "devicegroups", "description": "Cumulocity device groups", "descriptionLong": "REST endpoint to interact with Cumulocity device groups", "link": "https://cumulocity.com/guides/reference/inventory/" }, - "endpoints": [ + "commands": [ { "name": "listDeviceGroups", "description": "Get device group collection", @@ -105,7 +105,7 @@ }, { "name": "group", - "type": "[]devicegroup", + "type": "devicegroup[]", "description": "Filter by group inclusion", "format": "bygroupid(%s)" } @@ -183,7 +183,7 @@ "pathParameters": [ { "name": "id", - "type": "[]devicegroup", + "type": "devicegroup[]", "pipeline": true, "required": true, "description": "Device group ID" @@ -271,7 +271,7 @@ "pathParameters": [ { "name": "id", - "type": "[]devicegroup", + "type": "devicegroup[]", "pipeline": true, "required": true, "description": "Device group ID" @@ -329,7 +329,7 @@ "pathParameters": [ { "name": "id", - "type": "[]devicegroup", + "type": "devicegroup[]", "pipeline": true, "required": true, "description": "Device group ID", @@ -476,7 +476,7 @@ "pathParameters": [ { "name": "group", - "type": "[]devicegroup", + "type": "devicegroup[]", "property": "id", "required": true, "description": "Group" @@ -485,7 +485,7 @@ "body": [ { "name": "newChildDevice", - "type": "[]device", + "type": "device[]", "required": true, "pipeline": true, "property": "managedObject.id", @@ -557,7 +557,7 @@ "pathParameters": [ { "name": "group", - "type": "[]devicegroup", + "type": "devicegroup[]", "property": "id", "required": true, "description": "Group" @@ -566,7 +566,7 @@ "body": [ { "name": "newChildGroup", - "type": "[]devicegroup", + "type": "devicegroup[]", "pipeline": true, "required": true, "property": "managedObject.id", @@ -618,13 +618,13 @@ "pathParameters": [ { "name": "group", - "type": "[]devicegroup", + "type": "devicegroup[]", "required": true, "description": "Asset id" }, { "name": "childDevice", - "type": "[]device", + "type": "device[]", "property": "reference", "pipeline": true, "required": true, @@ -675,13 +675,13 @@ "pathParameters": [ { "name": "id", - "type": "[]devicegroup", + "type": "devicegroup[]", "required": true, "description": "Device group" }, { "name": "child", - "type": "[]devicegroup", + "type": "devicegroup[]", "property": "child", "required": true, "pipeline": true, @@ -747,7 +747,7 @@ "pathParameters": [ { "name": "id", - "type": "[]devicegroup", + "type": "devicegroup[]", "pipeline": true, "required": true, "description": "Device Group." diff --git a/api/spec/json/deviceGroupsChildren.json b/api/spec/json/deviceGroupsChildren.json index 4fd959f28..0388b28e4 100644 --- a/api/spec/json/deviceGroupsChildren.json +++ b/api/spec/json/deviceGroupsChildren.json @@ -1,11 +1,11 @@ { - "information": { + "group": { "name": "devicegroups/children", "description": "Cumulocity managed object child references", "descriptionLong": "Manage child entities (assets, additions and device) for device groups", "link": "https://cumulocity.com/guides/reference/inventory/" }, - "endpoints": [ + "commands": [ { "name": "listChildCollection", "method": "GET", @@ -46,7 +46,7 @@ "pathParameters": [ { "name": "id", - "type": "[]devicegroup", + "type": "devicegroup[]", "pipeline": true, "pipelineAliases": [ "deviceId", @@ -137,7 +137,7 @@ "pathParameters": [ { "name": "id", - "type": "[]devicegroup", + "type": "devicegroup[]", "property": "id", "required": true, "description": "Managed object id where the child will be assigned to", @@ -206,7 +206,7 @@ "pathParameters": [ { "name": "id", - "type": "[]devicegroup", + "type": "devicegroup[]", "required": true, "description": "Managed object id" }, @@ -274,7 +274,7 @@ "pathParameters": [ { "name": "id", - "type": "[]devicegroup", + "type": "devicegroup[]", "property": "id", "required": true, "pipeline": true, @@ -343,7 +343,7 @@ "pathParameters": [ { "name": "id", - "type": "[]devicegroup", + "type": "devicegroup[]", "pipeline": true, "pipelineAliases": [ "deviceId", @@ -367,7 +367,7 @@ }, { "name": "child", - "type": "[]devicegroup", + "type": "devicegroup[]", "required": true, "description": "Child managed object id" } diff --git a/api/spec/json/deviceProfile.json b/api/spec/json/deviceProfile.json index eb690e602..597830507 100644 --- a/api/spec/json/deviceProfile.json +++ b/api/spec/json/deviceProfile.json @@ -1,11 +1,11 @@ { - "information": { + "group": { "name": "deviceprofiles", "description": "Cumulocity device profile management", "descriptionLong": "Commands to managed Cumulocity device profiles", "link": "https://cumulocity.com/guides/users-guide/device-management/#managing-device-profiles" }, - "endpoints": [ + "commands": [ { "name": "getDeviceProfileCollection", "method": "GET", @@ -219,7 +219,7 @@ "pathParameters": [ { "name": "id", - "type": "[]deviceprofile", + "type": "deviceprofile[]", "pipeline": true, "required": true, "description": "DeviceProfile (managedObject) id" @@ -318,7 +318,7 @@ "pathParameters": [ { "name": "id", - "type": "[]deviceprofile", + "type": "deviceprofile[]", "pipeline": true, "description": "Device profile (managedObject) id", "required": true @@ -369,7 +369,7 @@ "pathParameters": [ { "name": "id", - "type": "[]deviceprofile", + "type": "deviceprofile[]", "pipeline": true, "required": true, "description": "DeviceProfile Package (managedObject) id" diff --git a/api/spec/json/deviceStatistics.json b/api/spec/json/deviceStatistics.json index 08775fb6f..06c499207 100644 --- a/api/spec/json/deviceStatistics.json +++ b/api/spec/json/deviceStatistics.json @@ -1,11 +1,11 @@ { - "information": { + "group": { "name": "devices/statistics", "description": "Cumulocity device statistics (for a single tenant) statistics", "descriptionLong": "Device statistics are collected for each inventory object with at least one measurement, event or alarm. There are no additional checks if the inventory object is marked as device using the c8y_IsDevice fragment. When the first measurement, event or alarm is created for a specific inventory object, Cumulocity IoT is always considering this as a device and starts counting.\n\nDevice statistics are counted with daily and monthly rate. All requests are considered when counting device statistics, no matter which processing mode is used.\n", "link": "https://cumulocity.com/api/latest/#tag/Device-statistics" }, - "endpoints": [ + "commands": [ { "name": "listStatisticsCollection", "description": "Retrieve device statistics", @@ -95,7 +95,7 @@ { "name": "device", "property": "deviceId", - "type": "[]device", + "type": "device[]", "description": "The ID of the device to search for.", "pipeline": true, "required": false diff --git a/api/spec/json/deviceUser.json b/api/spec/json/deviceUser.json index 0c73374b5..9df290d6e 100644 --- a/api/spec/json/deviceUser.json +++ b/api/spec/json/deviceUser.json @@ -1,11 +1,11 @@ { - "information": { + "group": { "name": "devices/user", "description": "Cumulocity device user management", "descriptionLong": "Managed the device user related to a device", "link": "https://cumulocity.com/guides/reference/inventory/" }, - "endpoints": [ + "commands": [ { "name": "getDeviceUser", "description": "Get device user", @@ -60,7 +60,7 @@ "pathParameters": [ { "name": "id", - "type": "[]device", + "type": "device[]", "pipeline": true, "required": true, "description": "Device ID" @@ -116,7 +116,7 @@ "pathParameters": [ { "name": "id", - "type": "[]device", + "type": "device[]", "pipeline": true, "required": true, "description": "Device ID" diff --git a/api/spec/json/devicecertificates.json b/api/spec/json/devicecertificates.json index 29ae207ff..24c1e59e4 100644 --- a/api/spec/json/devicecertificates.json +++ b/api/spec/json/devicecertificates.json @@ -1,11 +1,11 @@ { - "information": { + "group": { "name": "devicemanagement/certificates", "description": "Device Certificate management", "descriptionLong": "Manage the trusted certificates which are used by devices.", "link": "https://cumulocity.com/guides/users-guide/device-management/#trusted-certificates" }, - "endpoints": [ + "commands": [ { "name": "listCertificate", "description": "List device certificates", @@ -81,7 +81,7 @@ "pathParameters": [ { "name": "id", - "type": "[]certificate", + "type": "certificate[]", "pipeline": true, "required": false, "description": "Certificate fingerprint or name", @@ -143,7 +143,7 @@ "pathParameters": [ { "name": "id", - "type": "[]certificate", + "type": "certificate[]", "pipeline": true, "required": false, "description": "Certificate fingerprint or name", @@ -226,7 +226,7 @@ "pathParameters": [ { "name": "id", - "type": "[]certificate", + "type": "certificate[]", "pipeline": true, "required": false, "description": "Certificate fingerprint or name", diff --git a/api/spec/json/devicecontrol.json b/api/spec/json/devicecontrol.json index 2bd0d2cc1..c632d5c48 100644 --- a/api/spec/json/devicecontrol.json +++ b/api/spec/json/devicecontrol.json @@ -1,11 +1,11 @@ { - "information": { + "group": { "name": "operations", "description": "Cumulocity operations", "descriptionLong": "REST endpoint to interact with Cumulocity operations", "link": "https://cumulocity.com/guides/reference/device-control/" }, - "endpoints": [ + "commands": [ { "name": "getOperationCollection", "method": "GET", @@ -80,14 +80,14 @@ "queryParameters": [ { "name": "agent", - "type": "[]device", + "type": "device[]", "property": "agentId", "description": "Agent ID", "pipeline": false }, { "name": "device", - "type": "[]device", + "type": "device[]", "property": "deviceId", "description": "Device ID", "pipeline": true @@ -158,7 +158,7 @@ "pathParameters": [ { "name": "id", - "type": "[]id", + "type": "id[]", "required": true, "pipeline": true, "description": "Operation id" @@ -209,7 +209,7 @@ "body": [ { "name": "device", - "type": "[]device", + "type": "device[]", "property": "deviceId", "required": false, "pipeline": true, @@ -299,7 +299,7 @@ "pathParameters": [ { "name": "id", - "type": "[]id", + "type": "id[]", "description": "Operation id", "pipeline": true, "required": true @@ -334,13 +334,13 @@ "queryParameters": [ { "name": "agent", - "type": "[]device", + "type": "device[]", "property": "agentId", "description": "Agent ID" }, { "name": "device", - "type": "[]device", + "type": "device[]", "property": "deviceId", "description": "Device ID", "pipeline": true diff --git a/api/spec/json/devices.json b/api/spec/json/devices.json index d46ac6b80..7aa21a23e 100644 --- a/api/spec/json/devices.json +++ b/api/spec/json/devices.json @@ -1,11 +1,11 @@ { - "information": { + "group": { "name": "devices", "description": "Cumulocity devices", "descriptionLong": "REST endpoint to interact with Cumulocity devices", "link": "https://cumulocity.com/guides/reference/inventory/" }, - "endpoints": [ + "commands": [ { "name": "listDevices", "description": "Get device collection", @@ -173,7 +173,7 @@ }, { "name": "group", - "type": "[]devicegroup", + "type": "devicegroup[]", "description": "Filter by group inclusion", "format": "bygroupid(%s)" } @@ -250,7 +250,7 @@ "pathParameters": [ { "name": "id", - "type": "[]device", + "type": "device[]", "pipeline": true, "required": true, "description": "Device ID" @@ -353,7 +353,7 @@ "pathParameters": [ { "name": "id", - "type": "[]device", + "type": "device[]", "pipeline": true, "required": true, "description": "Device ID" @@ -437,7 +437,7 @@ "pathParameters": [ { "name": "id", - "type": "[]device", + "type": "device[]", "pipeline": true, "required": true, "description": "Device ID", @@ -594,7 +594,7 @@ "pathParameters": [ { "name": "device", - "type": "[]device", + "type": "device[]", "pipeline": true, "required": true, "description": "Device ID" @@ -648,7 +648,7 @@ "pathParameters": [ { "name": "device", - "type": "[]device", + "type": "device[]", "pipeline": true, "required": true, "description": "Device ID" @@ -692,7 +692,7 @@ "pathParameters": [ { "name": "device", - "type": "[]device", + "type": "device[]", "required": true, "description": "Device." } @@ -700,7 +700,7 @@ "body": [ { "name": "newChild", - "type": "[]device", + "type": "device[]", "required": true, "pipeline": true, "property": "managedObject.id", @@ -751,13 +751,13 @@ "pathParameters": [ { "name": "device", - "type": "[]device", + "type": "device[]", "required": true, "description": "ManagedObject id" }, { "name": "childDevice", - "type": "[]device", + "type": "device[]", "required": true, "pipeline": true, "description": "Child device reference" @@ -822,7 +822,7 @@ "pathParameters": [ { "name": "device", - "type": "[]device", + "type": "device[]", "pipeline": true, "required": true, "description": "Device." @@ -877,14 +877,14 @@ "pathParameters": [ { "name": "device", - "type": "[]device", + "type": "device[]", "pipeline": true, "required": true, "description": "ManagedObject id" }, { "name": "reference", - "type": "[]device", + "type": "device[]", "required": true, "description": "Device reference id" } @@ -950,7 +950,7 @@ "pathParameters": [ { "name": "id", - "type": "[]device", + "type": "device[]", "pipeline": true, "required": true, "description": "Device." diff --git a/api/spec/json/devicesChildren.json b/api/spec/json/devicesChildren.json index f82a240b5..bbae2bd22 100644 --- a/api/spec/json/devicesChildren.json +++ b/api/spec/json/devicesChildren.json @@ -1,11 +1,11 @@ { - "information": { + "group": { "name": "devices/children", "description": "Cumulocity managed object child references", "descriptionLong": "Manage child entities (assets, additions and device) for devices", "link": "https://cumulocity.com/guides/reference/inventory/" }, - "endpoints": [ + "commands": [ { "name": "listChildCollection", "method": "GET", @@ -46,7 +46,7 @@ "pathParameters": [ { "name": "id", - "type": "[]device", + "type": "device[]", "pipeline": true, "pipelineAliases": [ "deviceId", @@ -137,7 +137,7 @@ "pathParameters": [ { "name": "id", - "type": "[]device", + "type": "device[]", "property": "id", "required": true, "description": "Managed object id where the child will be assigned to", @@ -206,7 +206,7 @@ "pathParameters": [ { "name": "id", - "type": "[]device", + "type": "device[]", "required": true, "description": "Managed object id" }, @@ -274,7 +274,7 @@ "pathParameters": [ { "name": "id", - "type": "[]device", + "type": "device[]", "property": "id", "required": true, "pipeline": true, @@ -343,7 +343,7 @@ "pathParameters": [ { "name": "id", - "type": "[]device", + "type": "device[]", "pipeline": true, "pipelineAliases": [ "deviceId", @@ -367,7 +367,7 @@ }, { "name": "child", - "type": "[]device", + "type": "device[]", "required": true, "description": "Child managed object id" } diff --git a/api/spec/json/devicesServices.json b/api/spec/json/devicesServices.json index be03a946f..acf78eea7 100644 --- a/api/spec/json/devicesServices.json +++ b/api/spec/json/devicesServices.json @@ -1,11 +1,11 @@ { - "information": { + "group": { "name": "devices/services", "description": "Cumulocity device services", "descriptionLong": "Managed device services (introduced in 10.14)", "link": "https://cumulocity.com/guides/10.14.0/reference/device-management-library/#services" }, - "endpoints": [ + "commands": [ { "name": "findServices", "method": "GET", @@ -174,7 +174,7 @@ "pathParameters": [ { "name": "device", - "type": "[]device", + "type": "device[]", "pipeline": true, "pipelineAliases": [ "deviceId", @@ -276,7 +276,7 @@ "pathParameters": [ { "name": "device", - "type": "[]device", + "type": "device[]", "property": "id", "required": true, "pipeline": true, @@ -371,12 +371,12 @@ "pathParameters": [ { "name": "device", - "type": "[]device", + "type": "device[]", "description": "Device id (required for name lookup)" }, { "name": "id", - "type": "[]deviceservice", + "type": "deviceservice[]", "property": "id", "required": true, "pipeline": true, @@ -455,12 +455,12 @@ "pathParameters": [ { "name": "device", - "type": "[]device", + "type": "device[]", "description": "Device id (required for name lookup)" }, { "name": "id", - "type": "[]deviceservice", + "type": "deviceservice[]", "property": "id", "required": true, "pipeline": true, @@ -544,12 +544,12 @@ "pathParameters": [ { "name": "device", - "type": "[]device", + "type": "device[]", "description": "Device id (required for name lookup)" }, { "name": "id", - "type": "[]deviceservice", + "type": "deviceservice[]", "property": "id", "required": true, "pipeline": true, diff --git a/api/spec/json/events.json b/api/spec/json/events.json index 98dc35710..001dc6e18 100644 --- a/api/spec/json/events.json +++ b/api/spec/json/events.json @@ -1,11 +1,11 @@ { - "information": { + "group": { "name": "events", "description": "Cumulocity events", "descriptionLong": "REST endpoint to interact with Cumulocity events", "link": "https://cumulocity.com/guides/reference/events/" }, - "endpoints": [ + "commands": [ { "name": "getEventCollection", "method": "GET", @@ -69,7 +69,7 @@ "queryParameters": [ { "name": "device", - "type": "[]device", + "type": "device[]", "pipeline": true, "property": "source", "description": "Device ID" @@ -172,7 +172,7 @@ "queryParameters": [ { "name": "device", - "type": "[]device", + "type": "device[]", "property": "source", "pipeline": true, "description": "Device ID" @@ -242,7 +242,7 @@ "pathParameters": [ { "name": "id", - "type": "[]id", + "type": "id[]", "required": true, "pipeline": true, "description": "Event id" @@ -297,7 +297,7 @@ "body": [ { "name": "device", - "type": "[]device", + "type": "device[]", "required": false, "pipeline": true, "property": "source.id", @@ -403,7 +403,7 @@ "pathParameters": [ { "name": "id", - "type": "[]id", + "type": "id[]", "required": true, "pipeline": true, "description": "Event id" @@ -451,7 +451,7 @@ "pathParameters": [ { "name": "id", - "type": "[]id", + "type": "id[]", "required": true, "pipeline": true, "description": "Event id" @@ -502,7 +502,7 @@ "pathParameters": [ { "name": "id", - "type": "[]id", + "type": "id[]", "required": true, "pipeline": true, "description": "Event id" @@ -551,7 +551,7 @@ "pathParameters": [ { "name": "id", - "type": "[]id", + "type": "id[]", "required": true, "pipeline": true, "description": "Event id" @@ -608,7 +608,7 @@ "pathParameters": [ { "name": "id", - "type": "[]id", + "type": "id[]", "required": true, "pipeline": true, "description": "Event id" @@ -669,7 +669,7 @@ "pathParameters": [ { "name": "id", - "type": "[]id", + "type": "id[]", "required": true, "pipeline": true, "description": "Event id" diff --git a/api/spec/json/firmware.json b/api/spec/json/firmware.json index 5c843a5bc..ed58d6f26 100644 --- a/api/spec/json/firmware.json +++ b/api/spec/json/firmware.json @@ -1,11 +1,11 @@ { - "information": { + "group": { "name": "firmware", "description": "Cumulocity firmware management", "descriptionLong": "Firmware management to create/list/delete packages, versions and patches", "link": "https://cumulocity.com/guides/users-guide/device-management/#firmware-repo" }, - "endpoints": [ + "commands": [ { "name": "getFirmwareCollection", "method": "GET", @@ -269,7 +269,7 @@ "pathParameters": [ { "name": "id", - "type": "[]firmware", + "type": "firmware[]", "description": "Firmware package (managedObject) id", "required": true, "pipeline": true, @@ -388,7 +388,7 @@ "pathParameters": [ { "name": "id", - "type": "[]firmware", + "type": "firmware[]", "description": "Firmware package (managedObject) id", "required": true, "pipeline": true, @@ -451,7 +451,7 @@ "pathParameters": [ { "name": "id", - "type": "[]firmware", + "type": "firmware[]", "pipeline": true, "required": true, "pipelineAliases": [ diff --git a/api/spec/json/firmwarePatches.json b/api/spec/json/firmwarePatches.json index 330119b06..033963136 100644 --- a/api/spec/json/firmwarePatches.json +++ b/api/spec/json/firmwarePatches.json @@ -1,11 +1,11 @@ { - "information": { + "group": { "name": "firmware/patches", "description": "Cumulocity firmware patch management", "descriptionLong": "Firmware patch management to create/list/delete patches", "link": "https://cumulocity.com/guides/users-guide/device-management/#firmware-repo" }, - "endpoints": [ + "commands": [ { "name": "getFirmwarePatchCollection", "method": "GET", @@ -86,7 +86,7 @@ }, { "name": "firmware", - "type": "[]firmware", + "type": "firmware[]", "description": "Firmware package id or name", "format": "bygroupid(%s)", "required": true, @@ -195,7 +195,7 @@ "pathParameters": [ { "name": "id", - "type": "[]firmwarepatch", + "type": "firmwarepatch[]", "dependsOn": [ "firmware" ], @@ -205,7 +205,7 @@ }, { "name": "firmware", - "type": "[]firmware", + "type": "firmware[]", "required": false, "description": "Firmware package id or name (used to help completion be more accurate)" } @@ -300,7 +300,7 @@ "pathParameters": [ { "name": "id", - "type": "[]firmwarepatch", + "type": "firmwarepatch[]", "dependsOn": [ "firmware" ], @@ -310,7 +310,7 @@ }, { "name": "firmware", - "type": "[]firmware", + "type": "firmware[]", "required": false, "description": "Firmware id or name (used to help completion be more accurate)" } diff --git a/api/spec/json/firmwareVersions.json b/api/spec/json/firmwareVersions.json index e551e5d98..1f9f6552c 100644 --- a/api/spec/json/firmwareVersions.json +++ b/api/spec/json/firmwareVersions.json @@ -1,11 +1,11 @@ { - "information": { + "group": { "name": "firmware/versions", "description": "Cumulocity firmware version management", "descriptionLong": "Firmware version management to create/list/delete versions", "link": "https://cumulocity.com/guides/users-guide/device-management/#firmware-repo" }, - "endpoints": [ + "commands": [ { "name": "getFirmwareVersionCollection", "method": "GET", @@ -85,7 +85,7 @@ }, { "name": "firmware", - "type": "[]firmware", + "type": "firmware[]", "description": "Firmware package id or name", "format": "bygroupid(%s)", "required": true, @@ -190,7 +190,7 @@ "pathParameters": [ { "name": "id", - "type": "[]firmwareversion", + "type": "firmwareversion[]", "dependsOn": [ "firmware" ], @@ -200,7 +200,7 @@ }, { "name": "firmware", - "type": "[]firmware", + "type": "firmware[]", "required": false, "description": "Firmware package id or name (used to help completion be more accurate)" } @@ -281,7 +281,7 @@ "pathParameters": [ { "name": "id", - "type": "[]firmwareversion", + "type": "firmwareversion[]", "dependsOn": [ "firmware" ], @@ -291,7 +291,7 @@ }, { "name": "firmware", - "type": "[]firmware", + "type": "firmware[]", "required": false, "description": "Firmware package id or name (used to help completion be more accurate)" } @@ -346,7 +346,7 @@ "pathParameters": [ { "name": "url", - "type": "[]string", + "type": "string[]", "required": true, "pipeline": true, "description": "Firmware url", @@ -390,7 +390,7 @@ "body": [ { "name": "device", - "type": "[]device", + "type": "device[]", "property": "deviceId", "description": "Device or agent where the firmware should be installed", "pipeline": true @@ -404,7 +404,7 @@ }, { "name": "version", - "type": "firmwareVersionName", + "type": "firmwareversionName", "dependsOn": [ "firmware" ], diff --git a/api/spec/json/identity.json b/api/spec/json/identity.json index 168c2cc97..288bca621 100644 --- a/api/spec/json/identity.json +++ b/api/spec/json/identity.json @@ -1,11 +1,11 @@ { - "information": { + "group": { "name": "identity", "description": "Cumulocity external identity", "descriptionLong": "REST endpoint to interact with Cumulocity external identity objects", "link": "https://cumulocity.com/guides/reference/identity/" }, - "endpoints": [ + "commands": [ { "name": "getExternalIDCollection", "method": "GET", @@ -58,7 +58,7 @@ "pathParameters": [ { "name": "device", - "type": "[]device", + "type": "device[]", "required": true, "pipeline": true, "description": "Device id" @@ -244,7 +244,7 @@ "pathParameters": [ { "name": "device", - "type": "[]device", + "type": "device[]", "required": true, "pipeline": true, "description": "The ManagedObject linked to the external ID." diff --git a/api/spec/json/inventory.json b/api/spec/json/inventory.json index 1dbd20313..0972d636a 100644 --- a/api/spec/json/inventory.json +++ b/api/spec/json/inventory.json @@ -1,11 +1,11 @@ { - "information": { + "group": { "name": "inventory", "description": "Cumulocity managed objects", "descriptionLong": "REST endpoint to interact with Cumulocity managed objects", "link": "https://cumulocity.com/guides/reference/inventory/" }, - "endpoints": [ + "commands": [ { "name": "getManagedObjectCollection", "method": "GET", @@ -60,7 +60,7 @@ "queryParameters": [ { "name": "ids", - "type": "[]stringcsv", + "type": "stringcsv[]", "property": "ids", "description": "List of ids." }, @@ -106,7 +106,7 @@ }, { "name": "childDeviceId", - "type": "[]device", + "type": "device[]", "description": "Search for a specific child device and list all the groups to which it belongs." }, { @@ -180,7 +180,7 @@ "queryParameters": [ { "name": "ids", - "type": "[]stringcsv", + "type": "stringcsv[]", "property": "ids", "description": "List of ids." }, @@ -221,7 +221,7 @@ }, { "name": "childDeviceId", - "type": "[]device", + "type": "device[]", "description": "Search for a specific child device and list all the groups to which it belongs." } ] @@ -461,7 +461,7 @@ }, { "name": "group", - "type": "[]devicegroup", + "type": "devicegroup[]", "description": "Filter by group inclusion", "format": "bygroupid(%s)" }, @@ -614,7 +614,7 @@ "pathParameters": [ { "name": "id", - "type": "[]id", + "type": "id[]", "pipeline": true, "required": true, "description": "ManagedObject id", @@ -716,7 +716,7 @@ "pathParameters": [ { "name": "id", - "type": "[]id", + "type": "id[]", "pipeline": true, "description": "ManagedObject id", "required": true, @@ -797,7 +797,7 @@ "pathParameters": [ { "name": "id", - "type": "[]id", + "type": "id[]", "pipeline": true, "required": true, "description": "ManagedObject id", diff --git a/api/spec/json/inventoryAdditions.json b/api/spec/json/inventoryAdditions.json index 44432351f..a1904cadd 100644 --- a/api/spec/json/inventoryAdditions.json +++ b/api/spec/json/inventoryAdditions.json @@ -1,5 +1,5 @@ { - "information": { + "group": { "name": "inventory/additions", "description": "Cumulocity managed object additions", "descriptionLong": "Managed additions to managed objects", @@ -8,7 +8,7 @@ "deprecated": "please use 'c8y inventory children [command] --childType addition'", "hidden": true }, - "endpoints": [ + "commands": [ { "name": "listChildAdditionCollection", "method": "GET", @@ -64,7 +64,7 @@ "pathParameters": [ { "name": "id", - "type": "[]id", + "type": "id[]", "pipeline": true, "pipelineAliases": [ "deviceId", @@ -154,7 +154,7 @@ "pathParameters": [ { "name": "id", - "type": "[]id", + "type": "id[]", "property": "id", "required": true, "description": "Managed object id where the child addition will be added to", @@ -219,7 +219,7 @@ "pathParameters": [ { "name": "id", - "type": "[]id", + "type": "id[]", "required": true, "description": "Managed object id" }, @@ -284,7 +284,7 @@ "pathParameters": [ { "name": "id", - "type": "[]id", + "type": "id[]", "property": "id", "required": true, "pipeline": true, @@ -352,7 +352,7 @@ "pathParameters": [ { "name": "id", - "type": "[]id", + "type": "id[]", "pipeline": true, "pipelineAliases": [ "deviceId", @@ -365,7 +365,7 @@ }, { "name": "child", - "type": "[]id", + "type": "id[]", "required": true, "description": "Child managed object id" } diff --git a/api/spec/json/inventoryAssets.json b/api/spec/json/inventoryAssets.json index fc0d2b7c1..2921b0736 100644 --- a/api/spec/json/inventoryAssets.json +++ b/api/spec/json/inventoryAssets.json @@ -1,5 +1,5 @@ { - "information": { + "group": { "name": "inventory/assets", "description": "Cumulocity inventory assets", "descriptionLong": "REST endpoint to interact with Cumulocity managed objects", @@ -8,7 +8,7 @@ "deprecated": "please use 'c8y inventory children [command] --childType asset'", "hidden": true }, - "endpoints": [ + "commands": [ { "name": "listChildAssetCollection", "method": "GET", @@ -57,7 +57,7 @@ "pathParameters": [ { "name": "id", - "type": "[]id", + "type": "id[]", "required": true, "pipeline": true, "pipelineAliases": [ @@ -147,7 +147,7 @@ "pathParameters": [ { "name": "id", - "type": "[]id", + "type": "id[]", "property": "id", "required": true, "description": "Managed object id" @@ -156,7 +156,7 @@ "body": [ { "name": "childDevice", - "type": "[]device", + "type": "device[]", "pipeline": true, "required": false, "property": "managedObject.id", @@ -164,7 +164,7 @@ }, { "name": "childGroup", - "type": "[]devicegroup", + "type": "devicegroup[]", "required": false, "property": "managedObject.id", "description": "New child device group to be added to the group as an asset" @@ -214,7 +214,7 @@ "pathParameters": [ { "name": "id", - "type": "[]id", + "type": "id[]", "pipeline": true, "pipelineAliases": [ "deviceId", @@ -227,7 +227,7 @@ }, { "name": "child", - "type": "[]id", + "type": "id[]", "required": true, "description": "Child managed object id" } @@ -273,7 +273,7 @@ "pathParameters": [ { "name": "id", - "type": "[]id", + "type": "id[]", "description": "Asset id", "required": true }, diff --git a/api/spec/json/inventoryChildren.json b/api/spec/json/inventoryChildren.json index 5d72cf9ad..73cc8a890 100644 --- a/api/spec/json/inventoryChildren.json +++ b/api/spec/json/inventoryChildren.json @@ -1,11 +1,11 @@ { - "information": { + "group": { "name": "inventory/children", "description": "Cumulocity managed object child references", "descriptionLong": "Manage child entities (assets, additions and device) for managed objects", "link": "https://cumulocity.com/guides/reference/inventory/" }, - "endpoints": [ + "commands": [ { "name": "listChildCollection", "method": "GET", @@ -46,7 +46,7 @@ "pathParameters": [ { "name": "id", - "type": "[]id", + "type": "id[]", "pipeline": true, "pipelineAliases": [ "deviceId", @@ -137,7 +137,7 @@ "pathParameters": [ { "name": "id", - "type": "[]id", + "type": "id[]", "property": "id", "required": true, "description": "Managed object id where the child will be assigned to", @@ -206,7 +206,7 @@ "pathParameters": [ { "name": "id", - "type": "[]id", + "type": "id[]", "required": true, "description": "Managed object id" }, @@ -274,7 +274,7 @@ "pathParameters": [ { "name": "id", - "type": "[]id", + "type": "id[]", "property": "id", "required": true, "pipeline": true, @@ -343,7 +343,7 @@ "pathParameters": [ { "name": "id", - "type": "[]id", + "type": "id[]", "pipeline": true, "pipelineAliases": [ "deviceId", @@ -367,7 +367,7 @@ }, { "name": "child", - "type": "[]id", + "type": "id[]", "required": true, "description": "Child managed object id" } diff --git a/api/spec/json/measurements.json b/api/spec/json/measurements.json index 43fa1b20a..8126f564a 100644 --- a/api/spec/json/measurements.json +++ b/api/spec/json/measurements.json @@ -1,11 +1,11 @@ { - "information": { + "group": { "name": "measurements", "description": "Cumulocity measurements", "descriptionLong": "REST endpoint to interact with Cumulocity measurements", "link": "https://cumulocity.com/guides/reference/measurements/" }, - "endpoints": [ + "commands": [ { "name": "getMeasurementCollection", "method": "GET", @@ -84,7 +84,7 @@ "queryParameters": [ { "name": "device", - "type": "[]device", + "type": "device[]", "property": "source", "pipeline": true, "description": "Device ID" @@ -184,7 +184,7 @@ "queryParameters": [ { "name": "device", - "type": "[]device", + "type": "device[]", "property": "source", "pipeline": true, "description": "Device ID" @@ -248,14 +248,14 @@ "queryParameters": [ { "name": "device", - "type": "[]device", + "type": "device[]", "property": "source", "pipeline": true, "description": "Device ID" }, { "name": "series", - "type": "[]string", + "type": "string[]", "description": "measurement type and series name, e.g. c8y_AccelerationMeasurement.acceleration" }, { @@ -318,7 +318,7 @@ "pathParameters": [ { "name": "id", - "type": "[]id", + "type": "id[]", "required": true, "pipeline": true, "description": "Measurement id" @@ -376,7 +376,7 @@ "body": [ { "name": "device", - "type": "[]device", + "type": "device[]", "pipeline": true, "required": false, "property": "source.id", @@ -448,7 +448,7 @@ "pathParameters": [ { "name": "id", - "type": "[]id", + "type": "id[]", "required": true, "pipeline": true, "description": "Measurement id" @@ -489,7 +489,7 @@ "queryParameters": [ { "name": "device", - "type": "[]device", + "type": "device[]", "property": "source", "pipeline": true, "description": "Device ID" diff --git a/api/spec/json/microservices.json b/api/spec/json/microservices.json index 693dc6054..c0d731f24 100644 --- a/api/spec/json/microservices.json +++ b/api/spec/json/microservices.json @@ -1,11 +1,11 @@ { - "information": { + "group": { "name": "microservices", "description": "Cumulocity microservices", "descriptionLong": "REST endpoint to interact with Cumulocity microservices", "link": "https://cumulocity.com/guides/reference/applications/" }, - "endpoints": [ + "commands": [ { "name": "getMicroserviceCollection", "description": "Get microservice collection", @@ -78,7 +78,7 @@ }, { "name": "user", - "type": "[]user", + "type": "user[]", "description": "The ID of a user that has access to the applications.", "pipeline": true } diff --git a/api/spec/json/microservicesLoglevels.json b/api/spec/json/microservicesLoglevels.json index 9e3af5418..517564012 100644 --- a/api/spec/json/microservicesLoglevels.json +++ b/api/spec/json/microservicesLoglevels.json @@ -1,11 +1,11 @@ { - "information": { + "group": { "name": "microservices/loglevels", "description": "Cumulocity microservice log levels", "descriptionLong": "Manage log levels of microservices.\nLoggers define the log levels based on the qualified name of the Java class.\n(This only works for Spring Boot microservices based on Cumulocity Java Microservice SDK)\n", "link": "https://cumulocity.com/guides/reference/applications/" }, - "endpoints": [ + "commands": [ { "name": "list", "description": "List log levels of microservice", diff --git a/api/spec/json/notification2.json b/api/spec/json/notification2.json index ebfe7a7a7..bec35bcc5 100644 --- a/api/spec/json/notification2.json +++ b/api/spec/json/notification2.json @@ -1,9 +1,9 @@ { - "information": { + "group": { "name": "notification2", "description": "Cumulocity Notification2", "descriptionLong": "Managed tokens and subscriptions for notifications", "link": "https://cumulocity.com/guides/reference/notifications/" }, - "endpoints": [] + "commands": [] } diff --git a/api/spec/json/notification2Subscriptions.json b/api/spec/json/notification2Subscriptions.json index e238a6be2..cd2bde2fb 100644 --- a/api/spec/json/notification2Subscriptions.json +++ b/api/spec/json/notification2Subscriptions.json @@ -1,11 +1,11 @@ { - "information": { + "group": { "name": "notification2/subscriptions", "description": "Cumulocity notification2 subscriptions", "descriptionLong": "Methods to create, retrieve and delete notification subscriptions", "link": "https://cumulocity.com/guides/reference/notifications/" }, - "endpoints": [ + "commands": [ { "name": "listSubscriptions", "method": "GET", @@ -45,7 +45,7 @@ "queryParameters": [ { "name": "device", - "type": "[]device", + "type": "device[]", "pipeline": true, "property": "source", "description": "The managed object ID to which the subscription is associated." @@ -110,7 +110,7 @@ "body": [ { "name": "device", - "type": "[]device", + "type": "device[]", "pipeline": true, "property": "source.id", "description": "The managed object to which the subscription is associated." @@ -132,12 +132,12 @@ }, { "name": "fragmentsToCopy", - "type": "[]string", + "type": "string[]", "description": "Transforms the data to only include specified custom fragments. Each custom fragment is identified by a unique name. If nothing is specified here, the data is forwarded as-is." }, { "name": "apiFilter", - "type": "[]string", + "type": "string[]", "property": "subscriptionFilter.apis", "validationSet": [ "alarms", @@ -240,7 +240,7 @@ "queryParameters": [ { "name": "device", - "type": "[]device", + "type": "device[]", "pipeline": true, "property": "source", "description": "The managed object to which the subscription is associated." diff --git a/api/spec/json/notification2Tokens.json b/api/spec/json/notification2Tokens.json index 177cd7416..5de95d4f1 100644 --- a/api/spec/json/notification2Tokens.json +++ b/api/spec/json/notification2Tokens.json @@ -1,11 +1,11 @@ { - "information": { + "group": { "name": "notification2/tokens", "description": "Cumulocity notification2 tokens", "descriptionLong": "In order to receive subscribed notifications, a consumer application or microservice\nmust obtain an authorization token that provides proof that the holder is allowed to\nreceive subscribed notifications.\n", "link": "https://cumulocity.com/guides/reference/notifications/" }, - "endpoints": [ + "commands": [ { "name": "newToken", "method": "POST", diff --git a/api/spec/json/retentionRules.json b/api/spec/json/retentionRules.json index ba69587d6..38a0a6c29 100644 --- a/api/spec/json/retentionRules.json +++ b/api/spec/json/retentionRules.json @@ -1,11 +1,11 @@ { - "information": { + "group": { "name": "retentionRules", "description": "Cumulocity retentionRules", "descriptionLong": "REST endpoint to interact with Cumulocity retentionRules", "link": "https://cumulocity.com/guides/reference/retention-rules/#retention-rules" }, - "endpoints": [ + "commands": [ { "name": "getRetentionRuleCollection", "description": "Get retention rule collection", @@ -161,7 +161,7 @@ "pathParameters": [ { "name": "id", - "type": "[]id", + "type": "id[]", "required": true, "pipeline": true, "description": "Retention rule id" @@ -205,7 +205,7 @@ "pathParameters": [ { "name": "id", - "type": "[]id", + "type": "id[]", "required": true, "pipeline": true, "description": "Retention rule id" @@ -256,7 +256,7 @@ "pathParameters": [ { "name": "id", - "type": "[]id", + "type": "id[]", "required": true, "pipeline": true, "description": "Retention rule id" diff --git a/api/spec/json/smartgroups.json b/api/spec/json/smartgroups.json index 7fa360a72..e86785aad 100644 --- a/api/spec/json/smartgroups.json +++ b/api/spec/json/smartgroups.json @@ -1,11 +1,11 @@ { - "information": { + "group": { "name": "smartgroups", "description": "Cumulocity smart groups", "descriptionLong": "REST endpoint to interact with Cumulocity smart groups. A smart group is an inventory managed object and can also be managed via the Inventory api.", "link": "https://cumulocity.com/guides/reference/inventory/" }, - "endpoints": [ + "commands": [ { "name": "getSmartGroup", "description": "Get smart group", @@ -59,7 +59,7 @@ "pathParameters": [ { "name": "id", - "type": "[]smartgroup", + "type": "smartgroup[]", "pipeline": true, "required": true, "description": "Smart group ID" @@ -147,7 +147,7 @@ "pathParameters": [ { "name": "id", - "type": "[]smartgroup", + "type": "smartgroup[]", "pipeline": true, "required": true, "description": "Smart group ID" @@ -213,7 +213,7 @@ "pathParameters": [ { "name": "id", - "type": "[]smartgroup", + "type": "smartgroup[]", "pipeline": true, "required": true, "description": "Smart group ID", diff --git a/api/spec/json/software.json b/api/spec/json/software.json index 788912e08..89eefc363 100644 --- a/api/spec/json/software.json +++ b/api/spec/json/software.json @@ -1,11 +1,11 @@ { - "information": { + "group": { "name": "software", "description": "Cumulocity software management", "descriptionLong": "Software management to create/list/delete packages and versions", "link": "https://cumulocity.com/guides/users-guide/device-management/#software-repo" }, - "endpoints": [ + "commands": [ { "name": "getSoftwareCollection", "method": "GET", @@ -274,7 +274,7 @@ "pathParameters": [ { "name": "id", - "type": "[]software", + "type": "software[]", "pipeline": true, "required": true, "description": "Software package (managedObject) id" @@ -379,7 +379,7 @@ "pathParameters": [ { "name": "id", - "type": "[]software", + "type": "software[]", "pipeline": true, "description": "Software package (managedObject) id", "required": true @@ -434,7 +434,7 @@ "pathParameters": [ { "name": "id", - "type": "[]software", + "type": "software[]", "pipeline": true, "required": true, "description": "Software Package (managedObject) id" diff --git a/api/spec/json/softwareVersions.json b/api/spec/json/softwareVersions.json index 27614c869..d5d26c449 100644 --- a/api/spec/json/softwareVersions.json +++ b/api/spec/json/softwareVersions.json @@ -1,11 +1,11 @@ { - "information": { + "group": { "name": "software/versions", "description": "Cumulocity software version management", "descriptionLong": "Software version management to create/list/delete versions", "link": "https://cumulocity.com/guides/users-guide/device-management/#software-repo" }, - "endpoints": [ + "commands": [ { "name": "getSoftwareVersionCollection", "method": "GET", @@ -79,7 +79,7 @@ }, { "name": "software", - "type": "[]software", + "type": "software[]", "description": "Software package id or name", "format": "bygroupid(%s)", "pipeline": true, @@ -188,7 +188,7 @@ "pathParameters": [ { "name": "id", - "type": "[]softwareversion", + "type": "softwareversion[]", "dependsOn": [ "software" ], @@ -198,7 +198,7 @@ }, { "name": "software", - "type": "[]software", + "type": "software[]", "required": false, "description": "Software package id (used to help completion be more accurate)" } @@ -290,7 +290,7 @@ "pathParameters": [ { "name": "id", - "type": "[]softwareversion", + "type": "softwareversion[]", "dependsOn": [ "software" ], @@ -300,7 +300,7 @@ }, { "name": "software", - "type": "[]software", + "type": "software[]", "required": false, "description": "Software package id (used to help completion be more accurate)" } @@ -349,7 +349,7 @@ "body": [ { "name": "device", - "type": "[]device", + "type": "device[]", "property": "deviceId", "description": "Device or agent where the software should be installed", "pipeline": true @@ -445,7 +445,7 @@ "body": [ { "name": "device", - "type": "[]device", + "type": "device[]", "property": "deviceId", "description": "Device or agent where the software should be installed", "pipeline": true diff --git a/api/spec/json/systemOptions.json b/api/spec/json/systemOptions.json index 7d27a5248..ca9bf25ed 100644 --- a/api/spec/json/systemOptions.json +++ b/api/spec/json/systemOptions.json @@ -1,11 +1,11 @@ { - "information": { + "group": { "name": "systemOptions", "description": "Cumulocity systemOptions", "descriptionLong": "REST endpoint to interact with Cumulocity systemOptions", "link": "https://cumulocity.com/guides/reference/tenants/#system-options" }, - "endpoints": [ + "commands": [ { "name": "getSystemOptionCollection", "description": "Get system option collection", diff --git a/api/spec/json/tenantOptions.json b/api/spec/json/tenantOptions.json index 2ae9a261e..f4ae464c8 100644 --- a/api/spec/json/tenantOptions.json +++ b/api/spec/json/tenantOptions.json @@ -1,11 +1,11 @@ { - "information": { + "group": { "name": "tenantOptions", "description": "Cumulocity tenantOptions", "descriptionLong": "REST endpoint to interact with Cumulocity tenantOptions\nOptions are category-key-value tuples, storing tenant configuration. Some categories of options allow creation of new one, other are limited to predefined set of keys.\n\nAny option of any tenant can be defined as \"non-editable\" by \"management\" tenant. Afterwards, any PUT or DELETE requests made on that option by the owner tenant, will result in 403 error (Unauthorized).\n", "link": "https://cumulocity.com/guides/reference/tenants/#tenants" }, - "endpoints": [ + "commands": [ { "name": "getTenantOptionCollection", "description": "Get tenant option collection", diff --git a/api/spec/json/tenantStatistics.json b/api/spec/json/tenantStatistics.json index fbe9f0250..b7934bc07 100644 --- a/api/spec/json/tenantStatistics.json +++ b/api/spec/json/tenantStatistics.json @@ -1,11 +1,11 @@ { - "information": { + "group": { "name": "tenantStatistics", "description": "Cumulocity tenant statistics", "descriptionLong": "REST endpoint to interact with Cumulocity tenant statistics", "link": "https://cumulocity.com/guides/reference/tenants/#tenant-usage-statistics" }, - "endpoints": [ + "commands": [ { "name": "getTenantUsageStatisticsCollection", "description": "Get tenant usage statistics", diff --git a/api/spec/json/tenants.json b/api/spec/json/tenants.json index bac1f8060..db7ccd228 100644 --- a/api/spec/json/tenants.json +++ b/api/spec/json/tenants.json @@ -1,11 +1,11 @@ { - "information": { + "group": { "name": "tenants", "description": "Cumulocity tenant", "descriptionLong": "REST endpoint to interact with Cumulocity tenants", "link": "https://cumulocity.com/guides/reference/tenants/#tenants" }, - "endpoints": [ + "commands": [ { "name": "getTenantCollection", "description": "Get tenant collection", diff --git a/api/spec/json/userGroups.json b/api/spec/json/userGroups.json index bd4e1ea83..1575d475b 100644 --- a/api/spec/json/userGroups.json +++ b/api/spec/json/userGroups.json @@ -1,11 +1,11 @@ { - "information": { + "group": { "name": "userGroups", "description": "Cumulocity user groups", "descriptionLong": "REST endpoint to interact with Cumulocity user groups", "link": "https://cumulocity.com/guides/reference/users/#user-reference-collection" }, - "endpoints": [ + "commands": [ { "name": "getUserGroupCollection", "description": "Get user group collection", @@ -92,7 +92,7 @@ }, { "name": "deviceProperties", - "type": "[]string", + "type": "string[]", "description": "List of device permissions" }, { @@ -142,7 +142,7 @@ }, { "name": "id", - "type": "[]usergroup", + "type": "usergroup[]", "pipeline": true, "description": "Group id" } @@ -240,7 +240,7 @@ }, { "name": "id", - "type": "[]usergroup", + "type": "usergroup[]", "required": true, "pipeline": true, "description": "Group id" @@ -308,7 +308,7 @@ }, { "name": "id", - "type": "[]usergroup", + "type": "usergroup[]", "required": true, "pipeline": true, "description": "Group id" diff --git a/api/spec/json/userReferences.json b/api/spec/json/userReferences.json index cd29dfa2a..9ee5870b2 100644 --- a/api/spec/json/userReferences.json +++ b/api/spec/json/userReferences.json @@ -1,11 +1,11 @@ { - "information": { + "group": { "name": "userreferences", "description": "Cumulocity user references", "descriptionLong": "REST endpoint to interact with Cumulocity user references", "link": "https://cumulocity.com/guides/reference/users/#user-reference-collection" }, - "endpoints": [ + "commands": [ { "name": "addUserToGroup", "description": "Add user to group", @@ -60,7 +60,7 @@ "pathParameters": [ { "name": "group", - "type": "[]usergroup", + "type": "usergroup[]", "required": true, "description": "Group ID" }, @@ -74,7 +74,7 @@ "body": [ { "name": "user", - "type": "[]userself", + "type": "userself[]", "pipeline": true, "required": true, "property": "user.self", @@ -135,13 +135,13 @@ "pathParameters": [ { "name": "group", - "type": "[]usergroup", + "type": "usergroup[]", "required": true, "description": "Group ID" }, { "name": "user", - "type": "[]user", + "type": "user[]", "required": true, "pipeline": true, "description": "User id/username" @@ -214,7 +214,7 @@ "pathParameters": [ { "name": "id", - "type": "[]usergroup", + "type": "usergroup[]", "required": true, "pipeline": true, "description": "Group ID" diff --git a/api/spec/json/userRoles.json b/api/spec/json/userRoles.json index 7d03e5bfa..f93f02f1f 100644 --- a/api/spec/json/userRoles.json +++ b/api/spec/json/userRoles.json @@ -1,11 +1,11 @@ { - "information": { + "group": { "name": "userRoles", "description": "Cumulocity user roles", "descriptionLong": "REST endpoint to interact with Cumulocity user roles", "link": "https://cumulocity.com/guides/reference/users/#group-reference-collection" }, - "endpoints": [ + "commands": [ { "name": "getRoleCollection", "description": "Get role collection", @@ -99,7 +99,7 @@ }, { "name": "user", - "type": "[]user", + "type": "user[]", "required": true, "description": "User prefix or full username" } @@ -107,7 +107,7 @@ "body": [ { "name": "role", - "type": "[]roleself", + "type": "roleself[]", "pipeline": true, "property": "role.self", "description": "User role id", @@ -152,13 +152,13 @@ "pathParameters": [ { "name": "user", - "type": "[]user", + "type": "user[]", "required": true, "description": "User" }, { "name": "role", - "type": "[]role", + "type": "role[]", "required": true, "pipeline": true, "description": "Role name" @@ -228,7 +228,7 @@ }, { "name": "group", - "type": "[]usergroup", + "type": "usergroup[]", "required": true, "description": "Group ID" } @@ -236,7 +236,7 @@ "body": [ { "name": "role", - "type": "[]roleself", + "type": "roleself[]", "pipeline": true, "required": true, "property": "role.self", @@ -282,13 +282,13 @@ "pathParameters": [ { "name": "group", - "type": "[]usergroup", + "type": "usergroup[]", "required": true, "description": "Group id" }, { "name": "role", - "type": "[]role", + "type": "role[]", "required": true, "pipeline": true, "description": "Role name, e.g. ROLE_TENANT_MANAGEMENT_ADMIN" @@ -344,7 +344,7 @@ }, { "name": "user", - "type": "[]user", + "type": "user[]", "required": true, "pipeline": true, "description": "User" @@ -390,7 +390,7 @@ }, { "name": "group", - "type": "[]usergroup", + "type": "usergroup[]", "required": true, "pipeline": true, "description": "Group id" diff --git a/api/spec/json/users.json b/api/spec/json/users.json index f025d7f1d..002bdbcae 100644 --- a/api/spec/json/users.json +++ b/api/spec/json/users.json @@ -1,11 +1,11 @@ { - "information": { + "group": { "name": "users", "description": "Cumulocity users", "descriptionLong": "REST endpoint to interact with Cumulocity users", "link": "https://cumulocity.com/guides/reference/users/#user" }, - "endpoints": [ + "commands": [ { "name": "getInventoryRoleCollection", "description": "Get inventory role collection", @@ -62,7 +62,7 @@ "pathParameters": [ { "name": "id", - "type": "[]id", + "type": "id[]", "required": true, "pipeline": true, "description": "Role id. Note: lookup by name is not yet supported" @@ -281,7 +281,7 @@ "pathParameters": [ { "name": "id", - "type": "[]user", + "type": "user[]", "required": true, "pipeline": true, "description": "User id" @@ -381,7 +381,7 @@ }, { "name": "id", - "type": "[]user", + "type": "user[]", "required": true, "pipeline": true, "description": "User id" @@ -427,7 +427,7 @@ "pathParameters": [ { "name": "id", - "type": "[]user", + "type": "user[]", "required": true, "pipeline": true, "description": "User id" @@ -544,7 +544,7 @@ "pathParameters": [ { "name": "id", - "type": "[]user", + "type": "user[]", "required": true, "pipeline": true, "description": "User id" @@ -613,7 +613,7 @@ }, { "name": "id", - "type": "[]user", + "type": "user[]", "required": true, "pipeline": true, "description": "User id" @@ -653,7 +653,7 @@ "pathParameters": [ { "name": "id", - "type": "[]user", + "type": "user[]", "required": true, "pipeline": true, "description": "User" diff --git a/api/spec/schema.json b/api/spec/schema.json index c0c68de30..b8ee1b5ae 100644 --- a/api/spec/schema.json +++ b/api/spec/schema.json @@ -213,36 +213,36 @@ "subscriptionName", "tenant", "tenantname", - "[]devicerequest", - "[]deviceservice", - "[]id", - "[]agent", - "[]certificate", - "[]configuration", + "devicerequest[]", + "deviceservice[]", + "id[]", + "agent[]", + "certificate[]", + "configuration[]", "configurationDetails", - "[]deviceprofile", - "[]firmware", - "[]firmwareversion", - "[]firmwarepatch", + "deviceprofile[]", + "firmware[]", + "firmwareversion[]", + "firmwarepatch[]", "firmwareName", - "firmwareVersionName", + "firmwareversionName", "firmwareDetails", "firmwarepatchName", - "[]software", + "software[]", "softwareDetails", "softwareName", - "[]softwareversion", + "softwareversion[]", "softwareversionName", - "[]string", - "[]stringcsv", - "[]device", - "[]devicegroup", - "[]usergroup", - "[]userself", - "[]roleself", - "[]role", - "[]user", - "[]smartgroup" + "string[]", + "stringcsv[]", + "device[]", + "devicegroup[]", + "usergroup[]", + "userself[]", + "roleself[]", + "role[]", + "user[]", + "smartgroup[]" ] }, "pipelineAliases": { @@ -260,7 +260,13 @@ } }, "properties": { - "information": { + "version": { + "type": "string", + "title": "Specification version", + "description": "Specification version.", + "default": "v1" + }, + "group": { "type": "object", "properties": { "name": { @@ -309,7 +315,7 @@ "link" ] }, - "endpoints": { + "commands": { "type": "array", "minItems": 0, "items": { @@ -358,6 +364,10 @@ "type": "string", "description": "Accept header value. Data to be returned from the platform. The accept header will also control how the data will be displayed within the PowerShell module" }, + "contentType": { + "type": "string", + "description": "Content-Type header value. This is meant to be the MIME type of the body" + }, "collectionType": { "type": "string", "description": "Type of the raw response from the platform. The collectionType is the raw response type, and then the collectionProperty, is the type to be used for the array of items that are represented in the collectionType" @@ -427,6 +437,29 @@ } } }, + "exampleList": { + "type": "array", + "description": "List of examples", + "minItems": 1, + "items": { + "type": "object", + "properties": { + "description": { + "type": "string", + "title": "Description of what the example (command) does. This will be used in the auto generated documentation within the cli tool" + }, + "command": { + "type": "string", + "title": "Command line code which shows the usage of the command for the current endpoint", + "description": "This command should be what you run when using the native golang cli tool. i.e. mycliapp users list --pageSize 100" + } + }, + "required": [ + "description", + "command" + ] + } + }, "examples": { "type": "object", "properties": { @@ -742,7 +775,7 @@ } }, "required": [ - "information", - "endpoints" + "group", + "commands" ] } \ No newline at end of file diff --git a/api/spec/yaml/agents.yaml b/api/spec/yaml/agents.yaml index c775ae125..0e4a8dac5 100644 --- a/api/spec/yaml/agents.yaml +++ b/api/spec/yaml/agents.yaml @@ -1,12 +1,12 @@ # yaml-language-server: $schema=../schema.json --- -information: +group: name: agents description: Cumulocity agents descriptionLong: 'REST endpoint to interact with Cumulocity agents' link: https://cumulocity.com/guides/reference/inventory/ -endpoints: +commands: # Agent - name: listAgents description: Get agent collection @@ -126,7 +126,7 @@ endpoints: format: (creationTime.date ge '%s') - name: group - type: '[]devicegroup' + type: devicegroup[] description: Filter by group inclusion format: bygroupid(%s) @@ -181,7 +181,7 @@ endpoints: pathParameters: - name: id - type: '[]agent' + type: agent[] pipeline: true required: true description: Agent ID @@ -245,7 +245,7 @@ endpoints: pathParameters: - name: id - type: '[]agent' + type: agent[] pipeline: true required: true description: Agent ID @@ -309,7 +309,7 @@ endpoints: pathParameters: - name: id - type: '[]agent' + type: agent[] pipeline: true required: true description: Agent ID diff --git a/api/spec/yaml/alarms.yaml b/api/spec/yaml/alarms.yaml index c27b748a4..fa7b30863 100644 --- a/api/spec/yaml/alarms.yaml +++ b/api/spec/yaml/alarms.yaml @@ -1,12 +1,12 @@ # yaml-language-server: $schema=../schema.json --- -information: +group: name: alarms description: Cumulocity alarms descriptionLong: 'REST endpoint to interact with Cumulocity alarms' link: https://cumulocity.com/guides/reference/alarms/ -endpoints: +commands: - name: getAlarmCollection method: GET description: Get alarm collection @@ -50,7 +50,7 @@ endpoints: powershell: Get-AlarmCollection queryParameters: - name: device - type: '[]device' + type: device[] pipeline: true property: source description: Source device id. @@ -68,7 +68,7 @@ endpoints: description: Alarm type. - name: status - type: '[]stringcsv' + type: stringcsv[] description: Comma separated alarm statuses, for example ACTIVE,CLEARED. validationSet: [ACTIVE, ACKNOWLEDGED, CLEARED] @@ -135,7 +135,7 @@ endpoints: powershell: New-Alarm body: - name: device - type: '[]device' + type: device[] required: false pipeline: true property: source.id @@ -242,7 +242,7 @@ endpoints: queryParameters: - name: device - type: '[]device' + type: device[] property: source pipeline: true description: The ManagedObject that the alarm originated from @@ -288,7 +288,7 @@ endpoints: powershell: Get-Alarm pathParameters: - name: id - type: '[]id' + type: id[] pipeline: true required: true description: Alarm id @@ -358,7 +358,7 @@ endpoints: pathParameters: - name: id - type: '[]id' + type: id[] pipeline: true description: Alarm id required: true @@ -403,7 +403,7 @@ endpoints: powershell: Remove-AlarmCollection queryParameters: - name: device - type: '[]device' + type: device[] property: source pipeline: true description: 'Source device id.' @@ -431,7 +431,7 @@ endpoints: description: 'Alarm type.' - name: status - type: '[]stringcsv' + type: stringcsv[] description: Comma separated alarm statuses, for example ACTIVE,CLEARED. validationSet: [ACTIVE, ACKNOWLEDGED, CLEARED] @@ -506,7 +506,7 @@ endpoints: powershell: Get-AlarmCount queryParameters: - name: device - type: '[]device' + type: device[] pipeline: true property: source description: Source device id. @@ -524,7 +524,7 @@ endpoints: description: Alarm type. - name: status - type: '[]stringcsv' + type: stringcsv[] description: Comma separated alarm statuses, for example ACTIVE,CLEARED. validationSet: [ACTIVE, ACKNOWLEDGED, CLEARED] diff --git a/api/spec/yaml/applications.yaml b/api/spec/yaml/applications.yaml index 7979a8d89..912d981d7 100644 --- a/api/spec/yaml/applications.yaml +++ b/api/spec/yaml/applications.yaml @@ -1,13 +1,13 @@ # yaml-language-server: $schema=../schema.json --- -information: +group: name: applications description: Cumulocity applications descriptionLong: 'REST endpoint to interact with Cumulocity applications' link: https://cumulocity.com/guides/reference/applications/ -endpoints: +commands: - name: getApplicationCollection description: Get application collection descriptionLong: Get a collection of applications by a given filter @@ -58,7 +58,7 @@ endpoints: description: The ID of a tenant that is subscribed to the applications. - name: user - type: '[]user' + type: user[] description: The ID of a user that has access to the applications. @@ -468,7 +468,7 @@ endpoints: - name: binaryId alias: id - type: '[]id' + type: id[] pipeline: true required: true description: Application binary id diff --git a/api/spec/yaml/auditRecords.yaml b/api/spec/yaml/auditRecords.yaml index e31bcd2a9..3c361e375 100644 --- a/api/spec/yaml/auditRecords.yaml +++ b/api/spec/yaml/auditRecords.yaml @@ -1,13 +1,13 @@ # yaml-language-server: $schema=../schema.json --- -information: +group: name: auditRecords description: Cumulocity auditRecords descriptionLong: 'REST endpoint to interact with Cumulocity auditRecords' link: https://cumulocity.com/guides/reference/auditing/#audit-api -endpoints: +commands: - name: newAudit description: Create audit record descriptionLong: Create a new audit record for a given action @@ -194,7 +194,7 @@ endpoints: command: c8y auditrecords get --id 12345 pathParameters: - name: id - type: '[]id' + type: id[] required: true pipeline: true description: Audit id diff --git a/api/spec/yaml/binaries.yaml b/api/spec/yaml/binaries.yaml index 735fcccfa..94c1fff16 100644 --- a/api/spec/yaml/binaries.yaml +++ b/api/spec/yaml/binaries.yaml @@ -1,13 +1,13 @@ # yaml-language-server: $schema=../schema.json --- -information: +group: name: binaries description: Cumulocity binaries descriptionLong: 'REST endpoint to interact with Cumulocity binaries' link: https://cumulocity.com/guides/reference/binaries/ -endpoints: +commands: - name: getBinaryCollection description: Get binary collection descriptionLong: | @@ -39,7 +39,7 @@ endpoints: queryParameters: - name: ids - type: '[]stringcsv' + type: stringcsv[] property: ids description: The managed object IDs to search for. @@ -68,7 +68,7 @@ endpoints: description: Search for a specific child asset and list all the groups to which it belongs. - name: childDeviceId - type: '[]device' + type: device[] description: Search for a specific child device and list all the groups to which it belongs. - name: download @@ -112,7 +112,7 @@ endpoints: command: c8y binaries get --id 12345 --outputFileRaw "./download-binary1.txt" pathParameters: - name: id - type: '[]id' + type: id[] required: true pipeline: true description: Inventory binary id @@ -208,7 +208,7 @@ endpoints: command: c8y binaries update --id 12345 --file ./myfile.log pathParameters: - name: id - type: '[]id' + type: id[] required: true pipeline: true description: Inventory binary id @@ -245,7 +245,7 @@ endpoints: command: c8y binaries delete --id 12345 pathParameters: - name: id - type: '[]id' + type: id[] required: true pipeline: true description: Inventory binary id diff --git a/api/spec/yaml/bulkOperations.yaml b/api/spec/yaml/bulkOperations.yaml index 582aac0b6..b4c5e95f7 100644 --- a/api/spec/yaml/bulkOperations.yaml +++ b/api/spec/yaml/bulkOperations.yaml @@ -1,12 +1,12 @@ # yaml-language-server: $schema=../schema.json --- -information: +group: name: bulkOperations description: Cumulocity bulk operations descriptionLong: 'REST endpoint to interact with Cumulocity bulk operations' link: https://cumulocity.com/guides/reference/device-control/#bulk-operation-collection -endpoints: +commands: - name: getBulkOperationCollection method: GET description: Get bulk operation collection @@ -68,7 +68,7 @@ endpoints: - name: status property: generalStatus - type: '[]string' + type: string[] description: Operation status, can be one of SUCCESSFUL, FAILED, EXECUTING or PENDING. validationSet: [CANCELED, SCHEDULED, EXECUTING, EXECUTING_WITH_ERROR, FAILED] @@ -98,7 +98,7 @@ endpoints: powershell: Get-BulkOperation pathParameters: - name: id - type: '[]id' + type: id[] required: true pipeline: true description: Bulk Operation id @@ -127,7 +127,7 @@ endpoints: powershell: Remove-BulkOperation pathParameters: - name: id - type: '[]id' + type: id[] required: true pipeline: true description: Bulk Operation id @@ -167,7 +167,7 @@ endpoints: powershell: New-BulkOperation body: - name: group - type: '[]devicegroup' + type: devicegroup[] property: groupId required: false pipeline: true @@ -241,7 +241,7 @@ endpoints: pathParameters: # Note: id is actually an integer but it should be handled as a string because it is being applied to a path parameter (and piped input are strings) - name: id - type: '[]id' + type: id[] description: Bulk Operation id pipeline: true required: true @@ -285,7 +285,7 @@ endpoints: queryParameters: # Use string type, as an integer will default to 0 when not present in powershell - name: id - type: '[]id' + type: id[] property: bulkOperationId pipeline: true required: true diff --git a/api/spec/yaml/configuration.yaml b/api/spec/yaml/configuration.yaml index 005820142..c3d8f7de4 100644 --- a/api/spec/yaml/configuration.yaml +++ b/api/spec/yaml/configuration.yaml @@ -1,12 +1,12 @@ # yaml-language-server: $schema=../schema.json --- -information: +group: name: configuration description: Cumulocity configuration repository management descriptionLong: Configuration management to create/list/delete configurations link: https://cumulocity.com/guides/users-guide/device-management/#configuration-repository -endpoints: +commands: - name: getConfigurationCollection method: GET description: Get configuration collection @@ -208,7 +208,7 @@ endpoints: pathParameters: - name: id - type: '[]configuration' + type: configuration[] pipeline: true required: true description: Configuration package (managedObject) id @@ -301,7 +301,7 @@ endpoints: pathParameters: - name: id - type: '[]configuration' + type: configuration[] required: true pipeline: true description: Configuration package (managedObject) id @@ -349,7 +349,7 @@ endpoints: command: c8y configuration delete --id 12345 --forceCascade=false pathParameters: - name: id - type: '[]configuration' + type: configuration[] pipeline: true required: true description: Configuration file (managedObject) id @@ -403,7 +403,7 @@ endpoints: powershell: Send-Configuration body: - name: device - type: '[]device' + type: device[] property: deviceId required: false pipeline: true @@ -427,7 +427,7 @@ endpoints: description: Url to the configuration. Leave blank to automatically set it if a matching configuration is found in the c8y configuration repository - name: configuration - type: '[]configuration' + type: configuration[] property: __tmp_configuration required: false description: Configuration name or id diff --git a/api/spec/yaml/currentApplication.yaml b/api/spec/yaml/currentApplication.yaml index 209ea027e..6fe00029f 100644 --- a/api/spec/yaml/currentApplication.yaml +++ b/api/spec/yaml/currentApplication.yaml @@ -1,13 +1,13 @@ # yaml-language-server: $schema=../schema.json --- -information: +group: name: currentApplication description: Cumulocity currentApplication descriptionLong: 'REST endpoint to interact with Cumulocity currentApplication' link: https://cumulocity.com/guides/reference/applications/#current-application -endpoints: +commands: - name: getCurrentApplication description: 'Get current application' descriptionLong: | diff --git a/api/spec/yaml/currentTenant.yaml b/api/spec/yaml/currentTenant.yaml index d65293629..6af50d76f 100644 --- a/api/spec/yaml/currentTenant.yaml +++ b/api/spec/yaml/currentTenant.yaml @@ -1,13 +1,13 @@ # yaml-language-server: $schema=../schema.json --- -information: +group: name: currenttenant description: Cumulocity current tenant descriptionLong: 'Cumulocity current tenant commands' link: https://cumulocity.com/guides/reference/tenants/#tenants -endpoints: +commands: - name: currentTenant description: Get current tenant descriptionLong: Get the current tenant associated with the current session diff --git a/api/spec/yaml/currentUser.yaml b/api/spec/yaml/currentUser.yaml index 264b0a737..9974cb814 100644 --- a/api/spec/yaml/currentUser.yaml +++ b/api/spec/yaml/currentUser.yaml @@ -1,13 +1,13 @@ # yaml-language-server: $schema=../schema.json --- -information: +group: name: currentuser description: Cumulocity current user descriptionLong: 'REST endpoint to interact with the current Cumulocity user' link: https://cumulocity.com/guides/reference/users/#user -endpoints: +commands: - name: getCurrentUser description: Get current user descriptionLong: Get the user representation associated with the current credentials used by the request diff --git a/api/spec/yaml/databroker.yaml b/api/spec/yaml/databroker.yaml index 533d48f12..2bac0e305 100644 --- a/api/spec/yaml/databroker.yaml +++ b/api/spec/yaml/databroker.yaml @@ -1,13 +1,13 @@ # yaml-language-server: $schema=../schema.json --- -information: +group: name: databroker description: Cumulocity databroker descriptionLong: 'REST endpoint to interact with Cumulocity databroker' link: https://cumulocity.com/guides/users-guide/enterprise-tenant/#data-broker -endpoints: +commands: - name: getDataBrokerConnectorCollection description: Get data broker collection descriptionLong: Get a collection of existing data broker connectors @@ -50,7 +50,7 @@ endpoints: pathParameters: - name: id - type: '[]id' + type: id[] required: true pipeline: true description: Data broker connector id @@ -76,7 +76,7 @@ endpoints: pathParameters: - name: id - type: '[]id' + type: id[] required: true pipeline: true description: Data broker connector id diff --git a/api/spec/yaml/datahub.yaml b/api/spec/yaml/datahub.yaml index f6385563c..65c2eae27 100644 --- a/api/spec/yaml/datahub.yaml +++ b/api/spec/yaml/datahub.yaml @@ -1,6 +1,6 @@ # yaml-language-server: $schema=../schema.json --- -information: +group: name: datahub description: Cumulocity IoT Data Hub api descriptionLong: Data Hub api @@ -18,7 +18,7 @@ information: # /service/datahub/isalive => data.version # /service/datahub/system/properties => data.[EnvironmentVariables|SystemProperties] -endpoints: +commands: - name: query method: POST semanticMethod: GET diff --git a/api/spec/yaml/datahubConfiguration.yaml b/api/spec/yaml/datahubConfiguration.yaml index 7a27966dc..8f95e94d7 100644 --- a/api/spec/yaml/datahubConfiguration.yaml +++ b/api/spec/yaml/datahubConfiguration.yaml @@ -1,13 +1,13 @@ # yaml-language-server: $schema=../schema.json --- -information: +group: name: datahub/configuration description: Cumulocity IoT DataHub Configurations descriptionLong: Cumulocity IoT DataHub Configurations link: https://cumulocity.com/guides/datahub/datahub-overview/ skip: true -endpoints: +commands: - name: list method: GET path: service/datahub/offloadingconfigurations diff --git a/api/spec/yaml/datahubJobs.yaml b/api/spec/yaml/datahubJobs.yaml index 7f52208fe..19db4421e 100644 --- a/api/spec/yaml/datahubJobs.yaml +++ b/api/spec/yaml/datahubJobs.yaml @@ -1,12 +1,12 @@ # yaml-language-server: $schema=../schema.json --- -information: +group: name: datahub/jobs description: Cumulocity IoT DataHub Jobs descriptionLong: Cumulocity IoT DataHub Jobs link: https://cumulocity.com/guides/datahub/datahub-overview/ -endpoints: +commands: - name: createJob method: POST path: service/datahub/dremio/api/v3/sql @@ -51,7 +51,7 @@ endpoints: pipeline: true - name: context - type: '[]string' + type: string[] required: false description: The context in which the query is executed diff --git a/api/spec/yaml/datahubScheduler.yaml b/api/spec/yaml/datahubScheduler.yaml index c8518376f..aa8788b42 100644 --- a/api/spec/yaml/datahubScheduler.yaml +++ b/api/spec/yaml/datahubScheduler.yaml @@ -1,13 +1,13 @@ # yaml-language-server: $schema=../schema.json --- -information: +group: name: datahub/scheduler description: Cumulocity IoT DataHub Scheduler descriptionLong: Cumulocity IoT DataHub Scheduler link: https://cumulocity.com/guides/datahub/datahub-overview/ skip: true -endpoints: +commands: - name: list method: GET path: service/datahub/scheduler/latestjobs diff --git a/api/spec/yaml/datahubTenant.yaml b/api/spec/yaml/datahubTenant.yaml index c4ddc7798..08cf503da 100644 --- a/api/spec/yaml/datahubTenant.yaml +++ b/api/spec/yaml/datahubTenant.yaml @@ -1,13 +1,13 @@ # yaml-language-server: $schema=../schema.json --- -information: +group: name: datahub/tenant description: Cumulocity IoT DataHub Tenant information descriptionLong: Cumulocity IoT DataHub Tenant information link: https://cumulocity.com/guides/datahub/datahub-overview/ skip: true -endpoints: +commands: - name: get method: GET path: service/datahub/tenant diff --git a/api/spec/yaml/datahubUsers.yaml b/api/spec/yaml/datahubUsers.yaml index ff3436c4c..7cfc11842 100644 --- a/api/spec/yaml/datahubUsers.yaml +++ b/api/spec/yaml/datahubUsers.yaml @@ -1,13 +1,13 @@ # yaml-language-server: $schema=../schema.json --- -information: +group: name: datahub/users description: Cumulocity IoT DataHub Users descriptionLong: Cumulocity IoT DataHub Users link: https://cumulocity.com/guides/datahub/datahub-overview/ skip: true -endpoints: +commands: - name: list method: GET path: service/datahub/dremio/users diff --git a/api/spec/yaml/deviceAvailability.yaml b/api/spec/yaml/deviceAvailability.yaml index 02cf5afa8..2abc2e01d 100644 --- a/api/spec/yaml/deviceAvailability.yaml +++ b/api/spec/yaml/deviceAvailability.yaml @@ -1,12 +1,12 @@ # yaml-language-server: $schema=../schema.json --- -information: +group: name: devices/availability description: Cumulocity device availability descriptionLong: 'REST endpoint to interact with Cumulocity devices' link: https://cumulocity.com/guides/reference/inventory/ -endpoints: +commands: - name: setDeviceRequiredAvailability description: Set required availability @@ -51,7 +51,7 @@ endpoints: pathParameters: - name: id - type: '[]device' + type: device[] pipeline: true required: true description: Device ID @@ -102,7 +102,7 @@ endpoints: pathParameters: - name: id - type: '[]device' + type: device[] pipeline: true required: true description: Device. diff --git a/api/spec/yaml/deviceCredentials.yaml b/api/spec/yaml/deviceCredentials.yaml index 165002a09..92ad8a166 100644 --- a/api/spec/yaml/deviceCredentials.yaml +++ b/api/spec/yaml/deviceCredentials.yaml @@ -1,12 +1,12 @@ # yaml-language-server: $schema=../schema.json --- -information: +group: name: deviceregistration description: Cumulocity device credentials descriptionLong: 'REST endpoint to interact with Cumulocity device credentials api' link: https://cumulocity.com/guides/reference/device-credentials/ -endpoints: +commands: - name: getNewDeviceRequestCollection method: GET description: Get device request collection @@ -57,7 +57,7 @@ endpoints: pathParameters: - name: id - type: '[]devicerequest' + type: devicerequest[] required: true pipeline: true description: New Device Request ID @@ -83,7 +83,7 @@ endpoints: command: c8y deviceregistration register --id "ASDF098SD1J10912UD92JDLCNCU8" body: - name: id - type: '[]id' + type: id[] required: true pipeline: true description: 'Device identifier. Max: 1000 characters. E.g. IMEI' @@ -113,7 +113,7 @@ endpoints: command: c8y deviceregistration approve --id "1234010101s01ldk208" pathParameters: - name: id - type: '[]devicerequest' + type: devicerequest[] required: true pipeline: true description: 'Device identifier' @@ -152,7 +152,7 @@ endpoints: pathParameters: - name: id - type: '[]devicerequest' + type: devicerequest[] required: true pipeline: true description: New Device Request ID @@ -179,7 +179,7 @@ endpoints: command: c8y deviceregistration getCredentials --id "device-AD76-matrixer" body: - name: id - type: '[]devicerequest' + type: devicerequest[] required: true pipeline: true description: 'Device identifier. Max: 1000 characters. E.g. IMEI' diff --git a/api/spec/yaml/deviceGroups.yaml b/api/spec/yaml/deviceGroups.yaml index dc4f51e53..e9f562a2b 100644 --- a/api/spec/yaml/deviceGroups.yaml +++ b/api/spec/yaml/deviceGroups.yaml @@ -1,12 +1,12 @@ # yaml-language-server: $schema=../schema.json --- -information: +group: name: devicegroups description: Cumulocity device groups descriptionLong: 'REST endpoint to interact with Cumulocity device groups' link: https://cumulocity.com/guides/reference/inventory/ -endpoints: +commands: - name: listDeviceGroups description: Get device group collection @@ -85,7 +85,7 @@ endpoints: value: not(type eq 'c8y_DeviceGroup') - name: group - type: '[]devicegroup' + type: devicegroup[] description: Filter by group inclusion format: bygroupid(%s) @@ -142,7 +142,7 @@ endpoints: pathParameters: - name: id - type: '[]devicegroup' + type: devicegroup[] pipeline: true required: true description: Device group ID @@ -207,7 +207,7 @@ endpoints: pathParameters: - name: id - type: '[]devicegroup' + type: devicegroup[] pipeline: true required: true description: Device group ID @@ -250,7 +250,7 @@ endpoints: pathParameters: - name: id - type: '[]devicegroup' + type: devicegroup[] pipeline: true required: true description: Device group ID @@ -357,13 +357,13 @@ endpoints: pathParameters: - name: group - type: '[]devicegroup' + type: devicegroup[] property: id required: true description: Group body: - name: newChildDevice - type: '[]device' + type: device[] required: true pipeline: true property: 'managedObject.id' @@ -419,13 +419,13 @@ endpoints: pathParameters: - name: group - type: '[]devicegroup' + type: devicegroup[] property: id required: true description: Group body: - name: newChildGroup - type: '[]devicegroup' + type: devicegroup[] pipeline: true required: true property: 'managedObject.id' @@ -463,12 +463,12 @@ endpoints: command: c8y devicegroups unassignDevice --group 12345 --childDevice 22553 pathParameters: - name: group - type: '[]devicegroup' + type: devicegroup[] required: true description: Asset id - name: childDevice - type: '[]device' + type: device[] property: reference pipeline: true required: true @@ -505,12 +505,12 @@ endpoints: command: c8y devicegroups unassignGroup --id 12345 --child 22553 pathParameters: - name: id - type: '[]devicegroup' + type: devicegroup[] required: true description: Device group - name: child - type: '[]devicegroup' + type: devicegroup[] property: child required: true pipeline: true @@ -559,7 +559,7 @@ endpoints: command: c8y devicegroups listAssets --id 12345 pathParameters: - name: id - type: '[]devicegroup' + type: devicegroup[] pipeline: true required: true description: Device Group. diff --git a/api/spec/yaml/deviceGroupsChildren.yaml b/api/spec/yaml/deviceGroupsChildren.yaml index ab8953315..5b88ca2fc 100644 --- a/api/spec/yaml/deviceGroupsChildren.yaml +++ b/api/spec/yaml/deviceGroupsChildren.yaml @@ -1,12 +1,12 @@ # yaml-language-server: $schema=../schema.json --- -information: +group: name: devicegroups/children description: Cumulocity managed object child references descriptionLong: Manage child entities (assets, additions and device) for device groups link: https://cumulocity.com/guides/reference/inventory/ -endpoints: +commands: - name: listChildCollection method: GET description: Get child collection @@ -36,7 +36,7 @@ endpoints: command: c8y inventory children list --id 12345 --childType device pathParameters: - name: id - type: '[]devicegroup' + type: devicegroup[] pipeline: true pipelineAliases: - "deviceId" @@ -103,7 +103,7 @@ endpoints: command: c8y inventory children assign --id 12345 --child 6789 --childType addition pathParameters: - name: id - type: '[]devicegroup' + type: devicegroup[] property: id required: true description: Managed object id where the child will be assigned to @@ -154,7 +154,7 @@ endpoints: command: c8y inventory children unassign --id 12345 --child 22553 --childType device pathParameters: - name: id - type: '[]devicegroup' + type: devicegroup[] required: true description: Managed object id @@ -205,7 +205,7 @@ endpoints: pathParameters: - name: id - type: '[]devicegroup' + type: devicegroup[] property: id required: true pipeline: true @@ -255,7 +255,7 @@ endpoints: command: c8y inventory children get --id 12345 --child 12345 --childType addition pathParameters: - name: id - type: '[]devicegroup' + type: devicegroup[] pipeline: true pipelineAliases: - "deviceId" @@ -275,6 +275,6 @@ endpoints: - device - name: child - type: '[]devicegroup' + type: devicegroup[] required: true description: Child managed object id diff --git a/api/spec/yaml/deviceProfile.yaml b/api/spec/yaml/deviceProfile.yaml index eb5b680eb..6fec116b4 100644 --- a/api/spec/yaml/deviceProfile.yaml +++ b/api/spec/yaml/deviceProfile.yaml @@ -1,12 +1,12 @@ # yaml-language-server: $schema=../schema.json --- -information: +group: name: deviceprofiles description: Cumulocity device profile management descriptionLong: 'Commands to managed Cumulocity device profiles' link: https://cumulocity.com/guides/users-guide/device-management/#managing-device-profiles -endpoints: +commands: - name: getDeviceProfileCollection method: GET description: Get device profile collection @@ -163,7 +163,7 @@ endpoints: pathParameters: - name: id - type: '[]deviceprofile' + type: deviceprofile[] pipeline: true required: true description: DeviceProfile (managedObject) id @@ -236,7 +236,7 @@ endpoints: pathParameters: - name: id - type: '[]deviceprofile' + type: deviceprofile[] pipeline: true description: Device profile (managedObject) id required: true @@ -271,7 +271,7 @@ endpoints: command: c8y deviceprofiles delete --id 12345 pathParameters: - name: id - type: '[]deviceprofile' + type: deviceprofile[] pipeline: true required: true description: DeviceProfile Package (managedObject) id diff --git a/api/spec/yaml/deviceStatistics.yaml b/api/spec/yaml/deviceStatistics.yaml index 65002dbca..abc95f170 100644 --- a/api/spec/yaml/deviceStatistics.yaml +++ b/api/spec/yaml/deviceStatistics.yaml @@ -1,7 +1,7 @@ # yaml-language-server: $schema=../schema.json --- -information: +group: name: devices/statistics description: Cumulocity device statistics (for a single tenant) statistics descriptionLong: | @@ -10,7 +10,7 @@ information: Device statistics are counted with daily and monthly rate. All requests are considered when counting device statistics, no matter which processing mode is used. link: https://cumulocity.com/api/latest/#tag/Device-statistics -endpoints: +commands: - name: listStatisticsCollection description: Retrieve device statistics @@ -77,7 +77,7 @@ endpoints: queryParameters: - name: device property: deviceId - type: '[]device' + type: device[] description: The ID of the device to search for. pipeline: true required: false diff --git a/api/spec/yaml/deviceUser.yaml b/api/spec/yaml/deviceUser.yaml index 40c6314f4..eca6dee31 100644 --- a/api/spec/yaml/deviceUser.yaml +++ b/api/spec/yaml/deviceUser.yaml @@ -1,12 +1,12 @@ # yaml-language-server: $schema=../schema.json --- -information: +group: name: devices/user description: Cumulocity device user management descriptionLong: Managed the device user related to a device link: https://cumulocity.com/guides/reference/inventory/ -endpoints: +commands: - name: getDeviceUser description: Get device user descriptionLong: Retrieve the device owner's username and state (enabled or disabled) of a specific managed object @@ -45,7 +45,7 @@ endpoints: pathParameters: - name: id - type: '[]device' + type: device[] pipeline: true required: true description: Device ID @@ -86,7 +86,7 @@ endpoints: pathParameters: - name: id - type: '[]device' + type: device[] pipeline: true required: true description: Device ID diff --git a/api/spec/yaml/devicecertificates.yaml b/api/spec/yaml/devicecertificates.yaml index 93534b1d8..0dfce9b3a 100644 --- a/api/spec/yaml/devicecertificates.yaml +++ b/api/spec/yaml/devicecertificates.yaml @@ -1,12 +1,12 @@ # yaml-language-server: $schema=../schema.json --- -information: +group: name: devicemanagement/certificates description: Device Certificate management descriptionLong: Manage the trusted certificates which are used by devices. link: 'https://cumulocity.com/guides/users-guide/device-management/#trusted-certificates' -endpoints: +commands: - name: listCertificate description: 'List device certificates' descriptionLong: | @@ -62,7 +62,7 @@ endpoints: pathParameters: - name: id - type: '[]certificate' + type: certificate[] pipeline: true required: false description: Certificate fingerprint or name @@ -108,7 +108,7 @@ endpoints: pathParameters: - name: id - type: '[]certificate' + type: certificate[] pipeline: true required: false description: Certificate fingerprint or name @@ -171,7 +171,7 @@ endpoints: pathParameters: - name: id - type: '[]certificate' + type: certificate[] pipeline: true required: false description: Certificate fingerprint or name diff --git a/api/spec/yaml/devicecontrol.yaml b/api/spec/yaml/devicecontrol.yaml index 5aa89f98c..f4d77af59 100644 --- a/api/spec/yaml/devicecontrol.yaml +++ b/api/spec/yaml/devicecontrol.yaml @@ -1,12 +1,12 @@ # yaml-language-server: $schema=../schema.json --- -information: +group: name: operations description: Cumulocity operations descriptionLong: 'REST endpoint to interact with Cumulocity operations' link: https://cumulocity.com/guides/reference/device-control/ -endpoints: +commands: - name: getOperationCollection method: GET description: Get operation collection @@ -61,13 +61,13 @@ endpoints: command: c8y operations list --device 12345 --status PENDING queryParameters: - name: agent - type: '[]device' + type: device[] property: agentId description: Agent ID pipeline: false - name: device - type: '[]device' + type: device[] property: deviceId description: Device ID pipeline: true @@ -118,7 +118,7 @@ endpoints: powershell: Get-Operation pathParameters: - name: id - type: '[]id' + type: id[] required: true pipeline: true description: Operation id @@ -153,7 +153,7 @@ endpoints: powershell: New-Operation body: - name: device - type: '[]device' + type: device[] property: deviceId required: false pipeline: true @@ -216,7 +216,7 @@ endpoints: - status pathParameters: - name: id - type: '[]id' + type: id[] description: Operation id pipeline: true required: true @@ -243,12 +243,12 @@ endpoints: powershell: Remove-OperationCollection queryParameters: - name: agent - type: '[]device' + type: device[] property: agentId description: Agent ID - name: device - type: '[]device' + type: device[] property: deviceId description: Device ID pipeline: true diff --git a/api/spec/yaml/devices.yaml b/api/spec/yaml/devices.yaml index 6a9f1884e..2514eb925 100644 --- a/api/spec/yaml/devices.yaml +++ b/api/spec/yaml/devices.yaml @@ -1,12 +1,12 @@ # yaml-language-server: $schema=../schema.json --- -information: +group: name: devices description: Cumulocity devices descriptionLong: 'REST endpoint to interact with Cumulocity devices' link: https://cumulocity.com/guides/reference/inventory/ -endpoints: +commands: - name: listDevices description: Get device collection descriptionLong: Get a collection of devices based on filter parameters @@ -136,7 +136,7 @@ endpoints: format: (creationTime.date ge '%s') - name: group - type: '[]devicegroup' + type: devicegroup[] description: Filter by group inclusion format: bygroupid(%s) @@ -191,7 +191,7 @@ endpoints: pathParameters: - name: id - type: '[]device' + type: device[] pipeline: true required: true description: Device ID @@ -267,7 +267,7 @@ endpoints: pathParameters: - name: id - type: '[]device' + type: device[] pipeline: true required: true description: Device ID @@ -330,7 +330,7 @@ endpoints: pathParameters: - name: id - type: '[]device' + type: device[] pipeline: true required: true description: Device ID @@ -447,7 +447,7 @@ endpoints: pathParameters: - name: device - type: '[]device' + type: device[] pipeline: true required: true description: Device ID @@ -487,7 +487,7 @@ endpoints: pathParameters: - name: device - type: '[]device' + type: device[] pipeline: true required: true description: Device ID @@ -520,12 +520,12 @@ endpoints: command: c8y devices assignChild --device 12345 --newChild 44235 pathParameters: - name: device - type: '[]device' + type: device[] required: true description: Device. body: - name: newChild - type: '[]device' + type: device[] required: true pipeline: true property: 'managedObject.id' @@ -563,12 +563,12 @@ endpoints: command: c8y devices unassignChild --device 12345 --childDevice 22553 pathParameters: - name: device - type: '[]device' + type: device[] required: true description: ManagedObject id - name: childDevice - type: '[]device' + type: device[] required: true pipeline: true description: Child device reference @@ -616,7 +616,7 @@ endpoints: command: c8y devices listChildren --device 12345 pathParameters: - name: device - type: '[]device' + type: device[] pipeline: true required: true description: Device. @@ -655,13 +655,13 @@ endpoints: command: c8y devices getChild --device 12345 --reference 12345 pathParameters: - name: device - type: '[]device' + type: device[] pipeline: true required: true description: ManagedObject id - name: reference - type: '[]device' + type: device[] required: true description: Device reference id @@ -710,7 +710,7 @@ endpoints: command: c8y devices listAssets --id 12345 pathParameters: - name: id - type: '[]device' + type: device[] pipeline: true required: true description: Device. diff --git a/api/spec/yaml/devicesChildren.yaml b/api/spec/yaml/devicesChildren.yaml index e11ae6622..fc8ab8f2c 100644 --- a/api/spec/yaml/devicesChildren.yaml +++ b/api/spec/yaml/devicesChildren.yaml @@ -1,12 +1,12 @@ # yaml-language-server: $schema=../schema.json --- -information: +group: name: devices/children description: Cumulocity managed object child references descriptionLong: Manage child entities (assets, additions and device) for devices link: https://cumulocity.com/guides/reference/inventory/ -endpoints: +commands: - name: listChildCollection method: GET description: Get child collection @@ -36,7 +36,7 @@ endpoints: command: c8y inventory children list --id 12345 --childType device pathParameters: - name: id - type: '[]device' + type: device[] pipeline: true pipelineAliases: - "deviceId" @@ -103,7 +103,7 @@ endpoints: command: c8y inventory children assign --id 12345 --child 6789 --childType addition pathParameters: - name: id - type: '[]device' + type: device[] property: id required: true description: Managed object id where the child will be assigned to @@ -154,7 +154,7 @@ endpoints: command: c8y inventory children unassign --id 12345 --child 22553 --childType device pathParameters: - name: id - type: '[]device' + type: device[] required: true description: Managed object id @@ -205,7 +205,7 @@ endpoints: pathParameters: - name: id - type: '[]device' + type: device[] property: id required: true pipeline: true @@ -255,7 +255,7 @@ endpoints: command: c8y inventory children get --id 12345 --child 12345 --childType addition pathParameters: - name: id - type: '[]device' + type: device[] pipeline: true pipelineAliases: - "deviceId" @@ -275,6 +275,6 @@ endpoints: - device - name: child - type: '[]device' + type: device[] required: true description: Child managed object id diff --git a/api/spec/yaml/devicesServices.yaml b/api/spec/yaml/devicesServices.yaml index e1a6f2caf..ffeb8f25c 100644 --- a/api/spec/yaml/devicesServices.yaml +++ b/api/spec/yaml/devicesServices.yaml @@ -1,12 +1,12 @@ # yaml-language-server: $schema=../schema.json --- -information: +group: name: devices/services description: Cumulocity device services descriptionLong: Managed device services (introduced in 10.14) link: https://cumulocity.com/guides/10.14.0/reference/device-management-library/#services -endpoints: +commands: - name: findServices method: GET description: Find services @@ -134,7 +134,7 @@ endpoints: pathParameters: - name: device - type: '[]device' + type: device[] pipeline: true pipelineAliases: - "deviceId" @@ -211,7 +211,7 @@ endpoints: pathParameters: - name: device - type: '[]device' + type: device[] property: id required: true pipeline: true @@ -285,11 +285,11 @@ endpoints: pathParameters: - name: device - type: '[]device' + type: device[] description: Device id (required for name lookup) - name: id - type: '[]deviceservice' + type: deviceservice[] property: id required: true pipeline: true @@ -348,11 +348,11 @@ endpoints: pathParameters: - name: device - type: '[]device' + type: device[] description: Device id (required for name lookup) - name: id - type: '[]deviceservice' + type: deviceservice[] property: id required: true pipeline: true @@ -415,11 +415,11 @@ endpoints: pathParameters: - name: device - type: '[]device' + type: device[] description: Device id (required for name lookup) - name: id - type: '[]deviceservice' + type: deviceservice[] property: id required: true pipeline: true diff --git a/api/spec/yaml/events.yaml b/api/spec/yaml/events.yaml index 0f51a64c9..b75a1c5ea 100644 --- a/api/spec/yaml/events.yaml +++ b/api/spec/yaml/events.yaml @@ -1,12 +1,12 @@ # yaml-language-server: $schema=../schema.json --- -information: +group: name: events description: Cumulocity events descriptionLong: 'REST endpoint to interact with Cumulocity events' link: https://cumulocity.com/guides/reference/events/ -endpoints: +commands: - name: getEventCollection method: GET description: Get event collection @@ -53,7 +53,7 @@ endpoints: queryParameters: - name: device - type: '[]device' + type: device[] pipeline: true property: source description: Device ID @@ -131,7 +131,7 @@ endpoints: powershell: Remove-EventCollection queryParameters: - name: device - type: '[]device' + type: device[] property: source pipeline: true description: Device ID @@ -183,7 +183,7 @@ endpoints: powershell: Get-Event pathParameters: - name: id - type: '[]id' + type: id[] required: true pipeline: true description: Event id @@ -222,7 +222,7 @@ endpoints: powershell: New-Event body: - name: device - type: '[]device' + type: device[] required: false pipeline: true property: source.id @@ -300,7 +300,7 @@ endpoints: powershell: Update-Event pathParameters: - name: id - type: '[]id' + type: id[] required: true pipeline: true description: Event id @@ -333,7 +333,7 @@ endpoints: powershell: Remove-Event pathParameters: - name: id - type: '[]id' + type: id[] required: true pipeline: true description: Event id @@ -382,7 +382,7 @@ endpoints: pathParameters: - name: id - type: '[]id' + type: id[] required: true pipeline: true description: Event id @@ -419,7 +419,7 @@ endpoints: pathParameters: - name: id - type: '[]id' + type: id[] required: true pipeline: true description: Event id @@ -463,7 +463,7 @@ endpoints: pathParameters: - name: id - type: '[]id' + type: id[] required: true pipeline: true description: Event id @@ -505,7 +505,7 @@ endpoints: command: c8y events deleteBinary --id 12345 pathParameters: - name: id - type: '[]id' + type: id[] required: true pipeline: true description: Event id diff --git a/api/spec/yaml/firmware.yaml b/api/spec/yaml/firmware.yaml index 86b9e60fc..cda8b5502 100644 --- a/api/spec/yaml/firmware.yaml +++ b/api/spec/yaml/firmware.yaml @@ -1,12 +1,12 @@ # yaml-language-server: $schema=../schema.json --- -information: +group: name: firmware description: Cumulocity firmware management descriptionLong: Firmware management to create/list/delete packages, versions and patches link: https://cumulocity.com/guides/users-guide/device-management/#firmware-repo -endpoints: +commands: - name: getFirmwareCollection method: GET description: Get firmware collection @@ -204,7 +204,7 @@ endpoints: pathParameters: - name: id - type: '[]firmware' + type: firmware[] description: Firmware package (managedObject) id required: true pipeline: true @@ -293,7 +293,7 @@ endpoints: pathParameters: - name: id - type: '[]firmware' + type: firmware[] description: Firmware package (managedObject) id required: true pipeline: true @@ -339,7 +339,7 @@ endpoints: pathParameters: - name: id - type: '[]firmware' + type: firmware[] pipeline: true required: true pipelineAliases: diff --git a/api/spec/yaml/firmwarePatches.yaml b/api/spec/yaml/firmwarePatches.yaml index 054aa042b..ee7017cd8 100644 --- a/api/spec/yaml/firmwarePatches.yaml +++ b/api/spec/yaml/firmwarePatches.yaml @@ -1,12 +1,12 @@ # yaml-language-server: $schema=../schema.json --- -information: +group: name: firmware/patches description: Cumulocity firmware patch management descriptionLong: Firmware patch management to create/list/delete patches link: https://cumulocity.com/guides/users-guide/device-management/#firmware-repo -endpoints: +commands: - name: getFirmwarePatchCollection method: GET description: Get firmware patch collection @@ -67,7 +67,7 @@ endpoints: default: creationTime.date desc - name: firmware - type: '[]firmware' + type: firmware[] description: "Firmware package id or name" format: "bygroupid(%s)" required: true @@ -149,7 +149,7 @@ endpoints: pathParameters: - name: id - type: '[]firmwarepatch' + type: firmwarepatch[] dependsOn: - firmware pipeline: true @@ -158,7 +158,7 @@ endpoints: # Dummy value (to limit versions to the given package) - name: firmware - type: '[]firmware' + type: firmware[] required: false description: Firmware package id or name (used to help completion be more accurate) @@ -227,7 +227,7 @@ endpoints: command: c8y firmware patches delete --id 12345 --forceCascade=false pathParameters: - name: id - type: '[]firmwarepatch' + type: firmwarepatch[] dependsOn: - firmware pipeline: true @@ -236,7 +236,7 @@ endpoints: # Dummy value (to limit versions to the given package) - name: firmware - type: '[]firmware' + type: firmware[] required: false description: Firmware id or name (used to help completion be more accurate) diff --git a/api/spec/yaml/firmwareVersions.yaml b/api/spec/yaml/firmwareVersions.yaml index 28b0b2e28..d694649b5 100644 --- a/api/spec/yaml/firmwareVersions.yaml +++ b/api/spec/yaml/firmwareVersions.yaml @@ -1,12 +1,12 @@ # yaml-language-server: $schema=../schema.json --- -information: +group: name: firmware/versions description: Cumulocity firmware version management descriptionLong: Firmware version management to create/list/delete versions link: https://cumulocity.com/guides/users-guide/device-management/#firmware-repo -endpoints: +commands: - name: getFirmwareVersionCollection method: GET description: Get firmware package version collection @@ -67,7 +67,7 @@ endpoints: default: creationTime.date desc - name: firmware - type: '[]firmware' + type: firmware[] description: "Firmware package id or name" format: "bygroupid(%s)" required: true @@ -146,7 +146,7 @@ endpoints: pathParameters: - name: id - type: '[]firmwareversion' + type: firmwareversion[] dependsOn: - firmware pipeline: true @@ -155,7 +155,7 @@ endpoints: # Dummy value (to limit versions to the given package) - name: firmware - type: '[]firmware' + type: firmware[] required: false description: Firmware package id or name (used to help completion be more accurate) @@ -213,7 +213,7 @@ endpoints: command: c8y firmware versions delete --id 12345 --forceCascade=false pathParameters: - name: id - type: '[]firmwareversion' + type: firmwareversion[] dependsOn: - firmware pipeline: true @@ -222,7 +222,7 @@ endpoints: # Dummy value (to limit versions to the given package) - name: firmware - type: '[]firmware' + type: firmware[] required: false description: Firmware package id or name (used to help completion be more accurate) @@ -266,7 +266,7 @@ endpoints: skipTest: true pathParameters: - name: url - type: '[]string' + type: string[] required: true pipeline: true description: Firmware url @@ -300,7 +300,7 @@ endpoints: body: - name: device - type: '[]device' + type: device[] property: deviceId description: Device or agent where the firmware should be installed pipeline: true @@ -312,7 +312,7 @@ endpoints: description: Firmware name - name: version - type: firmwareVersionName + type: firmwareversionName dependsOn: - firmware required: false diff --git a/api/spec/yaml/identity.yaml b/api/spec/yaml/identity.yaml index af88d826a..d257ab50c 100644 --- a/api/spec/yaml/identity.yaml +++ b/api/spec/yaml/identity.yaml @@ -1,12 +1,12 @@ # yaml-language-server: $schema=../schema.json --- -information: +group: name: identity description: Cumulocity external identity descriptionLong: 'REST endpoint to interact with Cumulocity external identity objects' link: https://cumulocity.com/guides/reference/identity/ -endpoints: +commands: - name: getExternalIDCollection method: GET description: Get external identity collection @@ -46,7 +46,7 @@ endpoints: pathParameters: - name: device - type: '[]device' + type: device[] required: true pipeline: true description: Device id @@ -186,7 +186,7 @@ endpoints: pathParameters: - name: device - type: '[]device' + type: device[] required: true pipeline: true description: The ManagedObject linked to the external ID. diff --git a/api/spec/yaml/inventory.yaml b/api/spec/yaml/inventory.yaml index 25a920a12..a20732a2c 100644 --- a/api/spec/yaml/inventory.yaml +++ b/api/spec/yaml/inventory.yaml @@ -1,12 +1,12 @@ # yaml-language-server: $schema=../schema.json --- -information: +group: name: inventory description: Cumulocity managed objects descriptionLong: 'REST endpoint to interact with Cumulocity managed objects' link: https://cumulocity.com/guides/reference/inventory/ -endpoints: +commands: - name: getManagedObjectCollection method: GET description: Get managed object collection @@ -47,7 +47,7 @@ endpoints: queryParameters: - name: ids - type: '[]stringcsv' + type: stringcsv[] property: ids description: List of ids. @@ -84,7 +84,7 @@ endpoints: description: Search for a specific child asset and list all the groups to which it belongs. - name: childDeviceId - type: '[]device' + type: device[] description: Search for a specific child device and list all the groups to which it belongs. - name: skipChildrenNames @@ -138,7 +138,7 @@ endpoints: queryParameters: - name: ids - type: '[]stringcsv' + type: stringcsv[] property: ids description: List of ids. @@ -171,7 +171,7 @@ endpoints: description: Search for a specific child asset and list all the groups to which it belongs. - name: childDeviceId - type: '[]device' + type: device[] description: Search for a specific child device and list all the groups to which it belongs. @@ -360,7 +360,7 @@ endpoints: format: (creationTime.date ge '%s') - name: group - type: '[]devicegroup' + type: devicegroup[] description: Filter by group inclusion format: bygroupid(%s) @@ -470,7 +470,7 @@ endpoints: pathParameters: - name: id - type: '[]id' + type: id[] pipeline: true required: true description: ManagedObject id @@ -545,7 +545,7 @@ endpoints: pathParameters: - name: id - type: '[]id' + type: id[] pipeline: true description: ManagedObject id required: true @@ -604,7 +604,7 @@ endpoints: pathParameters: - name: id - type: '[]id' + type: id[] pipeline: true required: true description: ManagedObject id diff --git a/api/spec/yaml/inventoryAdditions.yaml b/api/spec/yaml/inventoryAdditions.yaml index fd890c493..b7f8ee43e 100644 --- a/api/spec/yaml/inventoryAdditions.yaml +++ b/api/spec/yaml/inventoryAdditions.yaml @@ -1,6 +1,6 @@ # yaml-language-server: $schema=../schema.json --- -information: +group: name: inventory/additions description: Cumulocity managed object additions descriptionLong: Managed additions to managed objects @@ -9,7 +9,7 @@ information: deprecated: "please use 'c8y inventory children [command] --childType addition'" hidden: true -endpoints: +commands: - name: listChildAdditionCollection method: GET description: Get child addition collection @@ -50,7 +50,7 @@ endpoints: command: c8y inventory additions list --id 12345 pathParameters: - name: id - type: '[]id' + type: id[] pipeline: true pipelineAliases: - "deviceId" @@ -115,7 +115,7 @@ endpoints: command: c8y inventory additions assign --id 12345 --child 6789 pathParameters: - name: id - type: '[]id' + type: id[] property: id required: true description: Managed object id where the child addition will be added to @@ -162,7 +162,7 @@ endpoints: command: c8y inventory additions unassign --id 12345 --child 22553 pathParameters: - name: id - type: '[]id' + type: id[] required: true description: Managed object id @@ -209,7 +209,7 @@ endpoints: pathParameters: - name: id - type: '[]id' + type: id[] property: id required: true pipeline: true @@ -257,7 +257,7 @@ endpoints: command: c8y inventory additions get --id 12345 --child 12345 pathParameters: - name: id - type: '[]id' + type: id[] pipeline: true pipelineAliases: - "deviceId" @@ -268,6 +268,6 @@ endpoints: description: Managed object id - name: child - type: '[]id' + type: id[] required: true description: Child managed object id diff --git a/api/spec/yaml/inventoryAssets.yaml b/api/spec/yaml/inventoryAssets.yaml index d3f999ad6..98a522746 100644 --- a/api/spec/yaml/inventoryAssets.yaml +++ b/api/spec/yaml/inventoryAssets.yaml @@ -1,6 +1,6 @@ # yaml-language-server: $schema=../schema.json --- -information: +group: name: inventory/assets description: Cumulocity inventory assets descriptionLong: 'REST endpoint to interact with Cumulocity managed objects' @@ -9,7 +9,7 @@ information: deprecated: "please use 'c8y inventory children [command] --childType asset'" hidden: true -endpoints: +commands: - name: listChildAssetCollection method: GET description: Get child asset collection @@ -46,7 +46,7 @@ endpoints: pathParameters: - name: id - type: '[]id' + type: id[] required: true pipeline: true pipelineAliases: @@ -111,20 +111,20 @@ endpoints: command: c8y inventory assets assign --id 12345 --childGroup 43234 pathParameters: - name: id - type: '[]id' + type: id[] property: id required: true description: Managed object id body: - name: childDevice - type: '[]device' + type: device[] pipeline: true required: false property: 'managedObject.id' description: New child device to be added to the group as an asset - name: childGroup - type: '[]devicegroup' + type: devicegroup[] required: false property: 'managedObject.id' description: New child device group to be added to the group as an asset @@ -159,7 +159,7 @@ endpoints: command: c8y inventory assets get --id 12345 --child 12345 pathParameters: - name: id - type: '[]id' + type: id[] pipeline: true pipelineAliases: - "deviceId" @@ -170,7 +170,7 @@ endpoints: description: Managed object id - name: child - type: '[]id' + type: id[] required: true description: Child managed object id @@ -202,7 +202,7 @@ endpoints: command: c8y inventory assets unassign --id 12345 --child 22553 pathParameters: - name: id - type: '[]id' + type: id[] description: Asset id required: true diff --git a/api/spec/yaml/inventoryChildren.yaml b/api/spec/yaml/inventoryChildren.yaml index c3849f80c..ac5f3ae3b 100644 --- a/api/spec/yaml/inventoryChildren.yaml +++ b/api/spec/yaml/inventoryChildren.yaml @@ -1,12 +1,12 @@ # yaml-language-server: $schema=../schema.json --- -information: +group: name: inventory/children description: Cumulocity managed object child references descriptionLong: Manage child entities (assets, additions and device) for managed objects link: https://cumulocity.com/guides/reference/inventory/ -endpoints: +commands: - name: listChildCollection method: GET description: Get child collection @@ -36,7 +36,7 @@ endpoints: command: c8y inventory children list --id 12345 --childType device pathParameters: - name: id - type: '[]id' + type: id[] pipeline: true pipelineAliases: - "deviceId" @@ -103,7 +103,7 @@ endpoints: command: c8y inventory children assign --id 12345 --child 6789 --childType addition pathParameters: - name: id - type: '[]id' + type: id[] property: id required: true description: Managed object id where the child will be assigned to @@ -154,7 +154,7 @@ endpoints: command: c8y inventory children unassign --id 12345 --child 22553 --childType device pathParameters: - name: id - type: '[]id' + type: id[] required: true description: Managed object id @@ -205,7 +205,7 @@ endpoints: pathParameters: - name: id - type: '[]id' + type: id[] property: id required: true pipeline: true @@ -255,7 +255,7 @@ endpoints: command: c8y inventory children get --id 12345 --child 12345 --childType addition pathParameters: - name: id - type: '[]id' + type: id[] pipeline: true pipelineAliases: - "deviceId" @@ -275,6 +275,6 @@ endpoints: - device - name: child - type: '[]id' + type: id[] required: true description: Child managed object id diff --git a/api/spec/yaml/measurements.yaml b/api/spec/yaml/measurements.yaml index f9961645a..cf1d4e6f1 100644 --- a/api/spec/yaml/measurements.yaml +++ b/api/spec/yaml/measurements.yaml @@ -1,12 +1,12 @@ # yaml-language-server: $schema=../schema.json --- -information: +group: name: measurements description: Cumulocity measurements descriptionLong: 'REST endpoint to interact with Cumulocity measurements' link: https://cumulocity.com/guides/reference/measurements/ -endpoints: +commands: - name: getMeasurementCollection method: GET description: Get measurement collection @@ -64,7 +64,7 @@ endpoints: queryParameters: - name: device - type: '[]device' + type: device[] property: source pipeline: true description: Device ID @@ -140,7 +140,7 @@ endpoints: queryParameters: - name: device - type: '[]device' + type: device[] property: source pipeline: true description: Device ID @@ -187,13 +187,13 @@ endpoints: command: c8y measurements getSeries --device 12345 --series app_Weather.temperature --series app_Weather.barometer --dateFrom "-10min" --dateTo "0s" queryParameters: - name: device - type: '[]device' + type: device[] property: source pipeline: true description: Device ID - name: series - type: "[]string" + type: string[] description: measurement type and series name, e.g. c8y_AccelerationMeasurement.acceleration - name: aggregationType @@ -236,7 +236,7 @@ endpoints: command: c8y measurements get --id 12345 pathParameters: - name: id - type: '[]id' + type: id[] required: true pipeline: true description: Measurement id @@ -283,7 +283,7 @@ endpoints: skipTest: true body: - name: device - type: '[]device' + type: device[] pipeline: true required: false property: source.id @@ -335,7 +335,7 @@ endpoints: command: c8y measurements delete --id 12345 pathParameters: - name: id - type: '[]id' + type: id[] required: true pipeline: true description: Measurement id @@ -363,7 +363,7 @@ endpoints: command: c8y measurements deleteCollection --device 12345 queryParameters: - name: device - type: '[]device' + type: device[] property: source pipeline: true description: Device ID diff --git a/api/spec/yaml/microservices.yaml b/api/spec/yaml/microservices.yaml index c11483348..6d7834e06 100644 --- a/api/spec/yaml/microservices.yaml +++ b/api/spec/yaml/microservices.yaml @@ -1,13 +1,13 @@ # yaml-language-server: $schema=../schema.json --- -information: +group: name: microservices description: Cumulocity microservices descriptionLong: 'REST endpoint to interact with Cumulocity microservices' link: https://cumulocity.com/guides/reference/applications/ -endpoints: +commands: - name: getMicroserviceCollection description: Get microservice collection descriptionLong: | @@ -62,7 +62,7 @@ endpoints: description: The ID of a tenant that is subscribed to the applications. - name: user - type: '[]user' + type: user[] description: The ID of a user that has access to the applications. pipeline: true diff --git a/api/spec/yaml/microservicesLoglevels.yaml b/api/spec/yaml/microservicesLoglevels.yaml index 5c9a26715..b84e77904 100644 --- a/api/spec/yaml/microservicesLoglevels.yaml +++ b/api/spec/yaml/microservicesLoglevels.yaml @@ -1,7 +1,7 @@ # yaml-language-server: $schema=../schema.json --- -information: +group: name: microservices/loglevels description: Cumulocity microservice log levels descriptionLong: | @@ -10,7 +10,7 @@ information: (This only works for Spring Boot microservices based on Cumulocity Java Microservice SDK) link: https://cumulocity.com/guides/reference/applications/ -endpoints: +commands: - name: list description: List log levels of microservice descriptionLong: | diff --git a/api/spec/yaml/notification2.yaml b/api/spec/yaml/notification2.yaml index 4a2233ab4..c22e05795 100644 --- a/api/spec/yaml/notification2.yaml +++ b/api/spec/yaml/notification2.yaml @@ -1,9 +1,9 @@ # yaml-language-server: $schema=../schema.json --- -information: +group: name: notification2 description: Cumulocity Notification2 descriptionLong: Managed tokens and subscriptions for notifications link: https://cumulocity.com/guides/reference/notifications/ -endpoints: [] +commands: [] diff --git a/api/spec/yaml/notification2Subscriptions.yaml b/api/spec/yaml/notification2Subscriptions.yaml index 0c026a496..4bbd908f1 100644 --- a/api/spec/yaml/notification2Subscriptions.yaml +++ b/api/spec/yaml/notification2Subscriptions.yaml @@ -1,12 +1,12 @@ # yaml-language-server: $schema=../schema.json --- -information: +group: name: notification2/subscriptions description: Cumulocity notification2 subscriptions descriptionLong: Methods to create, retrieve and delete notification subscriptions link: https://cumulocity.com/guides/reference/notifications/ -endpoints: +commands: - name: listSubscriptions method: GET description: Get subscription collection @@ -36,7 +36,7 @@ endpoints: powershell: Get-Notification2SubscriptionCollection queryParameters: - name: device - type: '[]device' + type: device[] pipeline: true property: source description: The managed object ID to which the subscription is associated. @@ -85,7 +85,7 @@ endpoints: powershell: New-Notification2Subscription body: - name: device - type: '[]device' + type: device[] pipeline: true property: source.id description: The managed object to which the subscription is associated. @@ -103,11 +103,11 @@ endpoints: description: The context to which the subscription is associated. - name: fragmentsToCopy - type: '[]string' + type: string[] description: Transforms the data to only include specified custom fragments. Each custom fragment is identified by a unique name. If nothing is specified here, the data is forwarded as-is. - name: apiFilter - type: '[]string' + type: string[] property: subscriptionFilter.apis validationSet: - "alarms" @@ -180,7 +180,7 @@ endpoints: powershell: Remove-Notification2SubscriptionBySource queryParameters: - name: device - type: '[]device' + type: device[] pipeline: true property: source description: The managed object to which the subscription is associated. diff --git a/api/spec/yaml/notification2Tokens.yaml b/api/spec/yaml/notification2Tokens.yaml index 5df1ed37c..30b8348c5 100644 --- a/api/spec/yaml/notification2Tokens.yaml +++ b/api/spec/yaml/notification2Tokens.yaml @@ -1,6 +1,6 @@ # yaml-language-server: $schema=../schema.json --- -information: +group: name: notification2/tokens description: Cumulocity notification2 tokens descriptionLong: | @@ -9,7 +9,7 @@ information: receive subscribed notifications. link: https://cumulocity.com/guides/reference/notifications/ -endpoints: +commands: - name: newToken method: POST path: notification2/token diff --git a/api/spec/yaml/retentionRules.yaml b/api/spec/yaml/retentionRules.yaml index d0a1abb73..54aa9c0e9 100644 --- a/api/spec/yaml/retentionRules.yaml +++ b/api/spec/yaml/retentionRules.yaml @@ -1,13 +1,13 @@ # yaml-language-server: $schema=../schema.json --- -information: +group: name: retentionRules description: Cumulocity retentionRules descriptionLong: 'REST endpoint to interact with Cumulocity retentionRules' link: https://cumulocity.com/guides/reference/retention-rules/#retention-rules -endpoints: +commands: - name: getRetentionRuleCollection description: Get retention rule collection descriptionLong: | @@ -118,7 +118,7 @@ endpoints: command: c8y retentionrules get --id 12345 pathParameters: - name: id - type: '[]id' + type: id[] required: true pipeline: true description: Retention rule id @@ -149,7 +149,7 @@ endpoints: command: c8y retentionrules delete --id 12345 pathParameters: - name: id - type: '[]id' + type: id[] required: true pipeline: true description: Retention rule id @@ -185,7 +185,7 @@ endpoints: command: c8y retentionrules update --id 12345 --maximumAge 90 pathParameters: - name: id - type: '[]id' + type: id[] required: true pipeline: true description: Retention rule id diff --git a/api/spec/yaml/smartgroups.yaml b/api/spec/yaml/smartgroups.yaml index a23cc082c..b3e06b64b 100644 --- a/api/spec/yaml/smartgroups.yaml +++ b/api/spec/yaml/smartgroups.yaml @@ -1,12 +1,12 @@ # yaml-language-server: $schema=../schema.json --- -information: +group: name: smartgroups description: Cumulocity smart groups descriptionLong: 'REST endpoint to interact with Cumulocity smart groups. A smart group is an inventory managed object and can also be managed via the Inventory api.' link: https://cumulocity.com/guides/reference/inventory/ -endpoints: +commands: - name: getSmartGroup description: Get smart group descriptionLong: Get an smart group @@ -44,7 +44,7 @@ endpoints: pathParameters: - name: id - type: '[]smartgroup' + type: smartgroup[] pipeline: true required: true description: Smart group ID @@ -108,7 +108,7 @@ endpoints: pathParameters: - name: id - type: '[]smartgroup' + type: smartgroup[] pipeline: true required: true description: Smart group ID @@ -158,7 +158,7 @@ endpoints: pathParameters: - name: id - type: '[]smartgroup' + type: smartgroup[] pipeline: true required: true description: Smart group ID diff --git a/api/spec/yaml/software.yaml b/api/spec/yaml/software.yaml index 7161e9152..0831caac2 100644 --- a/api/spec/yaml/software.yaml +++ b/api/spec/yaml/software.yaml @@ -1,12 +1,12 @@ # yaml-language-server: $schema=../schema.json --- -information: +group: name: software description: Cumulocity software management descriptionLong: Software management to create/list/delete packages and versions link: https://cumulocity.com/guides/users-guide/device-management/#software-repo -endpoints: +commands: - name: getSoftwareCollection method: GET description: Get software collection @@ -208,7 +208,7 @@ endpoints: pathParameters: - name: id - type: '[]software' + type: software[] pipeline: true required: true description: Software package (managedObject) id @@ -286,7 +286,7 @@ endpoints: pathParameters: - name: id - type: '[]software' + type: software[] pipeline: true description: Software package (managedObject) id required: true @@ -324,7 +324,7 @@ endpoints: command: c8y software delete --id 12345 --forceCascade=false pathParameters: - name: id - type: '[]software' + type: software[] pipeline: true required: true description: Software Package (managedObject) id diff --git a/api/spec/yaml/softwareVersions.yaml b/api/spec/yaml/softwareVersions.yaml index bf0b0ce61..bc123cd4d 100644 --- a/api/spec/yaml/softwareVersions.yaml +++ b/api/spec/yaml/softwareVersions.yaml @@ -1,12 +1,12 @@ # yaml-language-server: $schema=../schema.json --- -information: +group: name: software/versions description: Cumulocity software version management descriptionLong: Software version management to create/list/delete versions link: https://cumulocity.com/guides/users-guide/device-management/#software-repo -endpoints: +commands: - name: getSoftwareVersionCollection method: GET description: Get software package version collection @@ -62,7 +62,7 @@ endpoints: default: creationTime.date desc,creationTime desc - name: software - type: '[]software' + type: software[] description: "Software package id or name" format: "bygroupid(%s)" pipeline: true @@ -143,7 +143,7 @@ endpoints: pathParameters: - name: id - type: '[]softwareversion' + type: softwareversion[] dependsOn: - software pipeline: true @@ -152,7 +152,7 @@ endpoints: # Dummy value (to limit versions to the given package) - name: software - type: '[]software' + type: software[] required: false description: Software package id (used to help completion be more accurate) @@ -219,7 +219,7 @@ endpoints: command: c8y software versions delete --id 12345 --forceCascade=false pathParameters: - name: id - type: '[]softwareversion' + type: softwareversion[] dependsOn: - software pipeline: true @@ -227,7 +227,7 @@ endpoints: description: Software Package version id or name # Dummy value (to limit versions to the given package) - name: software - type: '[]software' + type: software[] required: false description: Software package id (used to help completion be more accurate) @@ -266,7 +266,7 @@ endpoints: body: - name: device - type: '[]device' + type: device[] property: deviceId description: Device or agent where the software should be installed pipeline: true @@ -345,7 +345,7 @@ endpoints: body: - name: device - type: '[]device' + type: device[] property: deviceId description: Device or agent where the software should be installed pipeline: true diff --git a/api/spec/yaml/systemOptions.yaml b/api/spec/yaml/systemOptions.yaml index a61ed978b..6d0977f1c 100644 --- a/api/spec/yaml/systemOptions.yaml +++ b/api/spec/yaml/systemOptions.yaml @@ -1,13 +1,13 @@ # yaml-language-server: $schema=../schema.json --- -information: +group: name: systemOptions description: Cumulocity systemOptions descriptionLong: 'REST endpoint to interact with Cumulocity systemOptions' link: https://cumulocity.com/guides/reference/tenants/#system-options -endpoints: +commands: - name: getSystemOptionCollection description: Get system option collection descriptionLong: Get a collection of system options. This endpoint provides a set of read-only properties pre-defined in platform configuration. The response format is exactly the same as for OptionCollection. diff --git a/api/spec/yaml/tenantOptions.yaml b/api/spec/yaml/tenantOptions.yaml index 1ebfcdaca..24a28675f 100644 --- a/api/spec/yaml/tenantOptions.yaml +++ b/api/spec/yaml/tenantOptions.yaml @@ -1,7 +1,7 @@ # yaml-language-server: $schema=../schema.json --- -information: +group: name: tenantOptions description: Cumulocity tenantOptions descriptionLong: | @@ -11,7 +11,7 @@ information: Any option of any tenant can be defined as "non-editable" by "management" tenant. Afterwards, any PUT or DELETE requests made on that option by the owner tenant, will result in 403 error (Unauthorized). link: https://cumulocity.com/guides/reference/tenants/#tenants -endpoints: +commands: - name: getTenantOptionCollection description: Get tenant option collection descriptionLong: Get collection of tenant options diff --git a/api/spec/yaml/tenantStatistics.yaml b/api/spec/yaml/tenantStatistics.yaml index 511e169f1..8318a12e5 100644 --- a/api/spec/yaml/tenantStatistics.yaml +++ b/api/spec/yaml/tenantStatistics.yaml @@ -1,13 +1,13 @@ # yaml-language-server: $schema=../schema.json --- -information: +group: name: tenantStatistics description: Cumulocity tenant statistics descriptionLong: 'REST endpoint to interact with Cumulocity tenant statistics' link: https://cumulocity.com/guides/reference/tenants/#tenant-usage-statistics -endpoints: +commands: - name: getTenantUsageStatisticsCollection description: Get tenant usage statistics descriptionLong: Get collection of tenant usage statistics diff --git a/api/spec/yaml/tenants.yaml b/api/spec/yaml/tenants.yaml index 9c16c6b81..f3bd2e08f 100644 --- a/api/spec/yaml/tenants.yaml +++ b/api/spec/yaml/tenants.yaml @@ -1,13 +1,13 @@ # yaml-language-server: $schema=../schema.json --- -information: +group: name: tenants description: Cumulocity tenant descriptionLong: 'REST endpoint to interact with Cumulocity tenants' link: https://cumulocity.com/guides/reference/tenants/#tenants -endpoints: +commands: - name: getTenantCollection description: Get tenant collection descriptionLong: Get collection of tenants diff --git a/api/spec/yaml/userGroups.yaml b/api/spec/yaml/userGroups.yaml index 0b5f85827..59d53d00d 100644 --- a/api/spec/yaml/userGroups.yaml +++ b/api/spec/yaml/userGroups.yaml @@ -1,13 +1,13 @@ # yaml-language-server: $schema=../schema.json --- -information: +group: name: userGroups description: Cumulocity user groups descriptionLong: 'REST endpoint to interact with Cumulocity user groups' link: https://cumulocity.com/guides/reference/users/#user-reference-collection -endpoints: +commands: - name: getUserGroupCollection description: Get user group collection descriptionLong: Get collection of (user) groups @@ -68,7 +68,7 @@ endpoints: - "name" - name: deviceProperties - type: '[]string' + type: string[] description: List of device permissions - name: data @@ -103,7 +103,7 @@ endpoints: position: 99 - name: id - type: '[]usergroup' + type: usergroup[] pipeline: true description: Group id @@ -171,7 +171,7 @@ endpoints: position: 99 - name: id - type: '[]usergroup' + type: usergroup[] required: true pipeline: true description: Group id @@ -220,7 +220,7 @@ endpoints: position: 99 - name: id - type: '[]usergroup' + type: usergroup[] required: true pipeline: true description: Group id diff --git a/api/spec/yaml/userReferences.yaml b/api/spec/yaml/userReferences.yaml index 327b68dc9..92fc0d77f 100644 --- a/api/spec/yaml/userReferences.yaml +++ b/api/spec/yaml/userReferences.yaml @@ -1,13 +1,13 @@ # yaml-language-server: $schema=../schema.json --- -information: +group: name: userreferences description: Cumulocity user references descriptionLong: 'REST endpoint to interact with Cumulocity user references' link: https://cumulocity.com/guides/reference/users/#user-reference-collection -endpoints: +commands: - name: addUserToGroup description: Add user to group descriptionLong: Add an existing user to a group @@ -45,7 +45,7 @@ endpoints: skipTest: true pathParameters: - name: group - type: '[]usergroup' + type: usergroup[] required: true description: Group ID @@ -56,7 +56,7 @@ endpoints: body: - name: user - type: '[]userself' + type: userself[] pipeline: true required: true property: user.self @@ -99,12 +99,12 @@ endpoints: path: r//user/$C8Y_TENANT/groups/1/users/peterpi@example.com pathParameters: - name: group - type: '[]usergroup' + type: usergroup[] required: true description: Group ID - name: user - type: '[]user' + type: user[] required: true pipeline: true description: User id/username @@ -157,7 +157,7 @@ endpoints: pathParameters: - name: id - type: '[]usergroup' + type: usergroup[] required: true pipeline: true description: Group ID diff --git a/api/spec/yaml/userRoles.yaml b/api/spec/yaml/userRoles.yaml index dca789678..3097155d1 100644 --- a/api/spec/yaml/userRoles.yaml +++ b/api/spec/yaml/userRoles.yaml @@ -1,13 +1,13 @@ # yaml-language-server: $schema=../schema.json --- -information: +group: name: userRoles description: Cumulocity user roles descriptionLong: 'REST endpoint to interact with Cumulocity user roles' link: https://cumulocity.com/guides/reference/users/#group-reference-collection -endpoints: +commands: - name: getRoleCollection description: Get role collection descriptionLong: Get collection of user roles @@ -71,13 +71,13 @@ endpoints: description: Tenant - name: user - type: '[]user' + type: user[] required: true description: User prefix or full username body: - name: role - type: '[]roleself' + type: roleself[] pipeline: true property: role.self description: User role id @@ -107,12 +107,12 @@ endpoints: command: c8y userroles deleteRoleFromUser --user "peterpi@example.com" --role "ROLE_MEASUREMENT_READ" pathParameters: - name: user - type: '[]user' + type: user[] required: true description: User - name: role - type: '[]role' + type: role[] required: true pipeline: true description: Role name @@ -162,13 +162,13 @@ endpoints: description: Tenant - name: group - type: '[]usergroup' + type: usergroup[] required: true description: Group ID body: - name: role - type: '[]roleself' + type: roleself[] pipeline: true required: true property: role.self @@ -199,12 +199,12 @@ endpoints: command: c8y userroles deleteRoleFromGroup --group 12345 --role "ROLE_MEASUREMENT_READ" pathParameters: - name: group - type: '[]usergroup' + type: usergroup[] required: true description: Group id - name: role - type: '[]role' + type: role[] required: true pipeline: true description: Role name, e.g. ROLE_TENANT_MANAGEMENT_ADMIN @@ -245,7 +245,7 @@ endpoints: description: Tenant - name: user - type: '[]user' + type: user[] required: true pipeline: true description: User @@ -277,7 +277,7 @@ endpoints: description: Tenant - name: group - type: '[]usergroup' + type: usergroup[] required: true pipeline: true description: Group id diff --git a/api/spec/yaml/users.yaml b/api/spec/yaml/users.yaml index d3fad291c..a33cfcc69 100644 --- a/api/spec/yaml/users.yaml +++ b/api/spec/yaml/users.yaml @@ -1,13 +1,13 @@ # yaml-language-server: $schema=../schema.json --- -information: +group: name: users description: Cumulocity users descriptionLong: 'REST endpoint to interact with Cumulocity users' link: https://cumulocity.com/guides/reference/users/#user -endpoints: +commands: # Inventory Roles - name: getInventoryRoleCollection description: Get inventory role collection @@ -46,7 +46,7 @@ endpoints: command: c8y users getInventoryRole --id 12345 pathParameters: - name: id - type: '[]id' + type: id[] required: true pipeline: true description: 'Role id. Note: lookup by name is not yet supported' @@ -210,7 +210,7 @@ endpoints: command: c8y users get --id "myuser" pathParameters: - name: id - type: '[]user' + type: user[] required: true pipeline: true description: User id @@ -282,7 +282,7 @@ endpoints: description: Tenant - name: id - type: '[]user' + type: user[] required: true pipeline: true description: User id @@ -315,7 +315,7 @@ endpoints: pathParameters: - name: id - type: '[]user' + type: user[] required: true pipeline: true description: User id @@ -404,7 +404,7 @@ endpoints: command: c8y users resetUserPassword --id "myuser" pathParameters: - name: id - type: '[]user' + type: user[] required: true pipeline: true description: User id @@ -456,7 +456,7 @@ endpoints: description: Tenant - name: id - type: '[]user' + type: user[] required: true pipeline: true description: User id @@ -487,7 +487,7 @@ endpoints: command: c8y users listUserMembership --id "myuser" pathParameters: - name: id - type: '[]user' + type: user[] required: true pipeline: true description: User diff --git a/cmd/gen-tests/main.go b/cmd/gen-tests/main.go index 2c9161cf8..2c39ebcf2 100644 --- a/cmd/gen-tests/main.go +++ b/cmd/gen-tests/main.go @@ -94,7 +94,7 @@ func NewGenerator(name string, mockConfig *models.MockConfiguration) (gen *Gener func (g *Generator) CreateTests(outDir string) error { - for _, endpoint := range g.Spec.Endpoints { + for _, endpoint := range g.Spec.Commands { if endpoint.Skip != nil && *endpoint.Skip { continue } @@ -108,7 +108,7 @@ func (g *Generator) CreateTests(outDir string) error { Skip: example.SkipTest, } - key := CreateKey(i, g.Spec.Information.Name, &endpoint) + key := CreateKey(i, g.Spec.Group.Name, &endpoint) testcase.Command = example.Command // Replace mock files @@ -121,7 +121,7 @@ func (g *Generator) CreateTests(outDir string) error { } loggerS.Debugf("Processing endpoint: %s", testcase.Command) - testcase.StdOut = buildAssertions(g.Spec.Information.Name, &endpoint, i) + testcase.StdOut = buildAssertions(g.Spec.Group.Name, &endpoint, i) testsuite.Tests[key] = *testcase } @@ -136,7 +136,7 @@ func (g *Generator) CreateTests(outDir string) error { return nil } -func parseCommand(parentCmd string, command string, endpoint *models.EndPoint) []string { +func parseCommand(parentCmd string, command string, endpoint *models.Command) []string { commands := strings.Split(command, "|") if len(commands) > 1 { currentCmd := "" @@ -154,7 +154,7 @@ func parseCommand(parentCmd string, command string, endpoint *models.EndPoint) [ } } -func CreateFakeCommand(parentCmd string, endpoint *models.EndPoint) *cobra.Command { +func CreateFakeCommand(parentCmd string, endpoint *models.Command) *cobra.Command { cmd := &cobra.Command{ Use: endpoint.Alias.Go, } @@ -196,7 +196,7 @@ func CreateFakeCommand(parentCmd string, endpoint *models.EndPoint) *cobra.Comma return cmd } -func parseFakeCommand(parentCmd string, command string, endpoint *models.EndPoint) *cobra.Command { +func parseFakeCommand(parentCmd string, command string, endpoint *models.Command) *cobra.Command { cmd := CreateFakeCommand(parentCmd, endpoint) if err := cmd.ParseFlags(parseCommand(parentCmd, command, endpoint)); err != nil { loggerS.Fatalf("Failed to parse command. command=%s, err=%s", command, err) @@ -205,7 +205,7 @@ func parseFakeCommand(parentCmd string, command string, endpoint *models.EndPoin return cmd } -func buildAssertions(parentCmd string, endpoint *models.EndPoint, exampleIdx int) (assertions *models.OutputAssertion) { +func buildAssertions(parentCmd string, endpoint *models.Command, exampleIdx int) (assertions *models.OutputAssertion) { cmd := parseFakeCommand(parentCmd, endpoint.Examples.Go[exampleIdx].Command, endpoint) assertions = &models.OutputAssertion{ @@ -253,7 +253,7 @@ func buildAssertions(parentCmd string, endpoint *models.EndPoint, exampleIdx int assertions.Contains = append(assertions.Contains, fmt.Sprintf("%s=", parameter.GetTargetProperty())) } else { switch parameter.Type { - case "[]string": + case "string[]": for _, v := range strings.Split(value, ",") { assertions.Contains = append(assertions.Contains, fmt.Sprintf("%s=%s", parameter.GetTargetProperty(), v)) } @@ -396,7 +396,7 @@ func getParameterValue(cmd *cobra.Command, parameter *models.Parameter) (value s return } -func substituteVariables(cmd *cobra.Command, endpoint *models.EndPoint) (out string) { +func substituteVariables(cmd *cobra.Command, endpoint *models.Command) (out string) { out = endpoint.Path if !strings.HasPrefix(out, "/") { @@ -457,11 +457,11 @@ func WriteTestSuite(t *models.TestSuite, id string, outDir string) (err error) { return } -func CreateSuiteKey(spec *models.Specification, endpoint *models.EndPoint) string { - return fmt.Sprintf("%s_%s", strings.ToLower(spec.Information.Name), endpoint.Alias.Go) +func CreateSuiteKey(spec *models.Specification, endpoint *models.Command) string { + return fmt.Sprintf("%s_%s", strings.ToLower(spec.Group.Name), endpoint.Alias.Go) } -func CreateKey(index int, parent string, endpoint *models.EndPoint) string { +func CreateKey(index int, parent string, endpoint *models.Command) string { return fmt.Sprintf( "%s_%s_%s", parent, @@ -503,8 +503,8 @@ func main() { } // Ignore skipped specs - if gen.Spec.Information.Skip { - loggerS.Fatalf("Specification is marked as skipped. Ignoring. file=%s", os.Args[2]) + if gen.Spec.Group.Skip { + loggerS.Warnf("Specification is marked as skipped. Ignoring. file=%s", os.Args[2]) os.Exit(0) } diff --git a/docs/go-c8y-cli/docs/cli/c8y/alias/c8y_alias_list.md b/docs/go-c8y-cli/docs/cli/c8y/alias/c8y_alias_list.md index 6208efbde..7b76dc32d 100644 --- a/docs/go-c8y-cli/docs/cli/c8y/alias/c8y_alias_list.md +++ b/docs/go-c8y-cli/docs/cli/c8y/alias/c8y_alias_list.md @@ -6,7 +6,7 @@ List your aliases ### Synopsis -This command prints out all of the aliases gh is configured to use. +This command prints out all of the aliases c8y is configured to use. ``` diff --git a/docs/go-c8y-cli/docs/cli/c8y/extensions/c8y_extensions.md b/docs/go-c8y-cli/docs/cli/c8y/extensions/c8y_extensions.md new file mode 100644 index 000000000..a25f82212 --- /dev/null +++ b/docs/go-c8y-cli/docs/cli/c8y/extensions/c8y_extensions.md @@ -0,0 +1,82 @@ +--- +category: extensions +title: c8y extensions +--- +Manage c8y extensions + +### Synopsis + +go-c8y-cli extensions are repositories that provide additional c8y commands. + +The name of the extension repository must start with "c8y-" and it must contain an +executable of the same name. All arguments passed to the `c8y ` invocation +will be forwarded to the `c8y-` executable of the extension. + +An extension cannot override any of the core c8y commands. + +See the list of available extensions at . + + +### Options + +``` + -h, --help help for extensions +``` + +### Options inherited from parent commands + +``` + --abortOnErrors int Abort batch when reaching specified number of errors (default 10) + --allowEmptyPipe Don't fail when piped input is empty (stdin) + --cache Enable cached responses + --cacheBodyPaths strings Cache should limit hashing of selected paths in the json body. Empty indicates all values + --cacheTTL string Cache time-to-live (TTL) as a duration, i.e. 60s, 2m (default "60s") + -c, --compact Compact instead of pretty-printed output when using json output. Pretty print is the default if output is the terminal + --confirm Prompt for confirmation + --confirmText string Custom confirmation text + --currentPage int Current page which should be returned + --customQueryParam strings add custom URL query parameters. i.e. --customQueryParam 'withCustomOption=true,myOtherOption=myvalue' + --debug Set very verbose log messages + --delay string delay after each request, i.e. 5ms, 1.2s (default "0ms") + --delayBefore string delay before each request, i.e. 5ms, 1.2s (default "0ms") + --dry Dry run. Don't send any data to the server + --dryFormat string Dry run output format. i.e. json, dump, markdown or curl (default "markdown") + --examples Show examples for the current command + --filter stringArray Apply a client side filter to response before returning it to the user + --flatten flatten json output by replacing nested json properties with properties where their names are represented by dot notation + -f, --force Do not prompt for confirmation. Ignored when using --confirm + -H, --header strings custom headers. i.e. --header "Accept: value, AnotherHeader: myvalue" + --includeAll Include all results by iterating through each page + -k, --insecure Allow insecure server connections when using SSL + -l, --logMessage string Add custom message to the activity log + --maxJobs int Maximum number of jobs. 0 = unlimited (use with caution!) + --noAccept Ignore Accept header will remove the Accept header from requests, however PUT and POST requests will only see the effect + --noCache Force disabling of cached responses (overwrites cache setting) + -M, --noColor Don't use colors when displaying log entries on the console + --noLog Disables the activity log for the current command + --noProgress Disable progress bars + --noProxy Ignore the proxy settings + -n, --nullInput Don't read the input (stdin). Useful if using in shell for/while loops + -o, --output string Output format i.e. table, json, csv, csvheader (default "table") + --outputFile string Save JSON output to file (after select/view) + --outputFileRaw string Save raw response to file (before select/view) + -p, --pageSize int Maximum results per page (default 5) + --progress Show progress bar. This will also disable any other verbose output + --proxy string Proxy setting, i.e. http://10.0.0.1:8080 + -r, --raw Show raw response. This mode will force output=json and view=off + --select stringArray Comma separated list of properties to return. wildcards and globstar accepted, i.e. --select 'id,name,type,**.serialNumber' + --session string Session configuration + -P, --sessionPassword string Override session password + -U, --sessionUsername string Override session username. i.e. peter or t1234/peter (with tenant) + --silentExit Silent status codes do not affect the exit code + --silentStatusCodes string Status codes which will not print out an error message + --timeout string Request timeout duration, i.e. 60s, 2m (default "60s") + --totalPages int Total number of pages to get + -v, --verbose Verbose logging + --view string Use views when displaying data on the terminal. Disable using --view off (default "auto") + --withError Errors will be printed on stdout instead of stderr + --withTotalElements Request Cumulocity to include the total elements in the response statistics under .statistics.totalElements (introduced in 10.13) + -t, --withTotalPages Request Cumulocity to include the total pages in the response statistics under .statistics.totalPages + --workers int Number of workers (default 1) +``` + diff --git a/docs/go-c8y-cli/docs/cli/c8y/extensions/c8y_extensions_create.md b/docs/go-c8y-cli/docs/cli/c8y/extensions/c8y_extensions_create.md new file mode 100644 index 000000000..ecd517221 --- /dev/null +++ b/docs/go-c8y-cli/docs/cli/c8y/extensions/c8y_extensions_create.md @@ -0,0 +1,84 @@ +--- +category: extensions +title: c8y extensions create +--- +Create a new extension + +``` +c8y extensions create [] [flags] +``` + +### Examples + +``` +# Use interactively +c8y extensions create + +# Create a script-based extension +c8y extensions create foobar + +``` + +### Options + +``` + -h, --help help for create +``` + +### Options inherited from parent commands + +``` + --abortOnErrors int Abort batch when reaching specified number of errors (default 10) + --allowEmptyPipe Don't fail when piped input is empty (stdin) + --cache Enable cached responses + --cacheBodyPaths strings Cache should limit hashing of selected paths in the json body. Empty indicates all values + --cacheTTL string Cache time-to-live (TTL) as a duration, i.e. 60s, 2m (default "60s") + -c, --compact Compact instead of pretty-printed output when using json output. Pretty print is the default if output is the terminal + --confirm Prompt for confirmation + --confirmText string Custom confirmation text + --currentPage int Current page which should be returned + --customQueryParam strings add custom URL query parameters. i.e. --customQueryParam 'withCustomOption=true,myOtherOption=myvalue' + --debug Set very verbose log messages + --delay string delay after each request, i.e. 5ms, 1.2s (default "0ms") + --delayBefore string delay before each request, i.e. 5ms, 1.2s (default "0ms") + --dry Dry run. Don't send any data to the server + --dryFormat string Dry run output format. i.e. json, dump, markdown or curl (default "markdown") + --examples Show examples for the current command + --filter stringArray Apply a client side filter to response before returning it to the user + --flatten flatten json output by replacing nested json properties with properties where their names are represented by dot notation + -f, --force Do not prompt for confirmation. Ignored when using --confirm + -H, --header strings custom headers. i.e. --header "Accept: value, AnotherHeader: myvalue" + --includeAll Include all results by iterating through each page + -k, --insecure Allow insecure server connections when using SSL + -l, --logMessage string Add custom message to the activity log + --maxJobs int Maximum number of jobs. 0 = unlimited (use with caution!) + --noAccept Ignore Accept header will remove the Accept header from requests, however PUT and POST requests will only see the effect + --noCache Force disabling of cached responses (overwrites cache setting) + -M, --noColor Don't use colors when displaying log entries on the console + --noLog Disables the activity log for the current command + --noProgress Disable progress bars + --noProxy Ignore the proxy settings + -n, --nullInput Don't read the input (stdin). Useful if using in shell for/while loops + -o, --output string Output format i.e. table, json, csv, csvheader (default "table") + --outputFile string Save JSON output to file (after select/view) + --outputFileRaw string Save raw response to file (before select/view) + -p, --pageSize int Maximum results per page (default 5) + --progress Show progress bar. This will also disable any other verbose output + --proxy string Proxy setting, i.e. http://10.0.0.1:8080 + -r, --raw Show raw response. This mode will force output=json and view=off + --select stringArray Comma separated list of properties to return. wildcards and globstar accepted, i.e. --select 'id,name,type,**.serialNumber' + --session string Session configuration + -P, --sessionPassword string Override session password + -U, --sessionUsername string Override session username. i.e. peter or t1234/peter (with tenant) + --silentExit Silent status codes do not affect the exit code + --silentStatusCodes string Status codes which will not print out an error message + --timeout string Request timeout duration, i.e. 60s, 2m (default "60s") + --totalPages int Total number of pages to get + -v, --verbose Verbose logging + --view string Use views when displaying data on the terminal. Disable using --view off (default "auto") + --withError Errors will be printed on stdout instead of stderr + --withTotalElements Request Cumulocity to include the total elements in the response statistics under .statistics.totalElements (introduced in 10.13) + -t, --withTotalPages Request Cumulocity to include the total pages in the response statistics under .statistics.totalPages + --workers int Number of workers (default 1) +``` + diff --git a/docs/go-c8y-cli/docs/cli/c8y/extensions/c8y_extensions_delete.md b/docs/go-c8y-cli/docs/cli/c8y/extensions/c8y_extensions_delete.md new file mode 100644 index 000000000..0abf008ab --- /dev/null +++ b/docs/go-c8y-cli/docs/cli/c8y/extensions/c8y_extensions_delete.md @@ -0,0 +1,73 @@ +--- +category: extensions +title: c8y extensions delete +--- +Remove an installed extension + +``` +c8y extensions delete [flags] +``` + +### Options + +``` + -h, --help help for delete +``` + +### Options inherited from parent commands + +``` + --abortOnErrors int Abort batch when reaching specified number of errors (default 10) + --allowEmptyPipe Don't fail when piped input is empty (stdin) + --cache Enable cached responses + --cacheBodyPaths strings Cache should limit hashing of selected paths in the json body. Empty indicates all values + --cacheTTL string Cache time-to-live (TTL) as a duration, i.e. 60s, 2m (default "60s") + -c, --compact Compact instead of pretty-printed output when using json output. Pretty print is the default if output is the terminal + --confirm Prompt for confirmation + --confirmText string Custom confirmation text + --currentPage int Current page which should be returned + --customQueryParam strings add custom URL query parameters. i.e. --customQueryParam 'withCustomOption=true,myOtherOption=myvalue' + --debug Set very verbose log messages + --delay string delay after each request, i.e. 5ms, 1.2s (default "0ms") + --delayBefore string delay before each request, i.e. 5ms, 1.2s (default "0ms") + --dry Dry run. Don't send any data to the server + --dryFormat string Dry run output format. i.e. json, dump, markdown or curl (default "markdown") + --examples Show examples for the current command + --filter stringArray Apply a client side filter to response before returning it to the user + --flatten flatten json output by replacing nested json properties with properties where their names are represented by dot notation + -f, --force Do not prompt for confirmation. Ignored when using --confirm + -H, --header strings custom headers. i.e. --header "Accept: value, AnotherHeader: myvalue" + --includeAll Include all results by iterating through each page + -k, --insecure Allow insecure server connections when using SSL + -l, --logMessage string Add custom message to the activity log + --maxJobs int Maximum number of jobs. 0 = unlimited (use with caution!) + --noAccept Ignore Accept header will remove the Accept header from requests, however PUT and POST requests will only see the effect + --noCache Force disabling of cached responses (overwrites cache setting) + -M, --noColor Don't use colors when displaying log entries on the console + --noLog Disables the activity log for the current command + --noProgress Disable progress bars + --noProxy Ignore the proxy settings + -n, --nullInput Don't read the input (stdin). Useful if using in shell for/while loops + -o, --output string Output format i.e. table, json, csv, csvheader (default "table") + --outputFile string Save JSON output to file (after select/view) + --outputFileRaw string Save raw response to file (before select/view) + -p, --pageSize int Maximum results per page (default 5) + --progress Show progress bar. This will also disable any other verbose output + --proxy string Proxy setting, i.e. http://10.0.0.1:8080 + -r, --raw Show raw response. This mode will force output=json and view=off + --select stringArray Comma separated list of properties to return. wildcards and globstar accepted, i.e. --select 'id,name,type,**.serialNumber' + --session string Session configuration + -P, --sessionPassword string Override session password + -U, --sessionUsername string Override session username. i.e. peter or t1234/peter (with tenant) + --silentExit Silent status codes do not affect the exit code + --silentStatusCodes string Status codes which will not print out an error message + --timeout string Request timeout duration, i.e. 60s, 2m (default "60s") + --totalPages int Total number of pages to get + -v, --verbose Verbose logging + --view string Use views when displaying data on the terminal. Disable using --view off (default "auto") + --withError Errors will be printed on stdout instead of stderr + --withTotalElements Request Cumulocity to include the total elements in the response statistics under .statistics.totalElements (introduced in 10.13) + -t, --withTotalPages Request Cumulocity to include the total pages in the response statistics under .statistics.totalPages + --workers int Number of workers (default 1) +``` + diff --git a/docs/go-c8y-cli/docs/cli/c8y/extensions/c8y_extensions_install.md b/docs/go-c8y-cli/docs/cli/c8y/extensions/c8y_extensions_install.md new file mode 100644 index 000000000..4a62f7b48 --- /dev/null +++ b/docs/go-c8y-cli/docs/cli/c8y/extensions/c8y_extensions_install.md @@ -0,0 +1,97 @@ +--- +category: extensions +title: c8y extensions install +--- +Install a c8y extensions from a repository + +### Synopsis + +Install a GitHub repository locally as a Cumulocity CLI extension. + +The repository argument can be specified in "owner/repo" format as well as a full URL. +The URL format is useful when the repository is not hosted on github.com. + +To install an extension in development from the current directory, use "." as the +value of the repository argument. + +See the list of available extensions at . + + +``` +c8y extensions install [flags] +``` + +### Examples + +``` +$ c8y extensions install owner/c8y-extension +$ c8y extensions install https://git.example.com/owner/c8y-extension +$ c8y extensions install . + +``` + +### Options + +``` + -h, --help help for install + --name string use custom name for the extension + --pin string pin extension to a release tag or commit ref +``` + +### Options inherited from parent commands + +``` + --abortOnErrors int Abort batch when reaching specified number of errors (default 10) + --allowEmptyPipe Don't fail when piped input is empty (stdin) + --cache Enable cached responses + --cacheBodyPaths strings Cache should limit hashing of selected paths in the json body. Empty indicates all values + --cacheTTL string Cache time-to-live (TTL) as a duration, i.e. 60s, 2m (default "60s") + -c, --compact Compact instead of pretty-printed output when using json output. Pretty print is the default if output is the terminal + --confirm Prompt for confirmation + --confirmText string Custom confirmation text + --currentPage int Current page which should be returned + --customQueryParam strings add custom URL query parameters. i.e. --customQueryParam 'withCustomOption=true,myOtherOption=myvalue' + --debug Set very verbose log messages + --delay string delay after each request, i.e. 5ms, 1.2s (default "0ms") + --delayBefore string delay before each request, i.e. 5ms, 1.2s (default "0ms") + --dry Dry run. Don't send any data to the server + --dryFormat string Dry run output format. i.e. json, dump, markdown or curl (default "markdown") + --examples Show examples for the current command + --filter stringArray Apply a client side filter to response before returning it to the user + --flatten flatten json output by replacing nested json properties with properties where their names are represented by dot notation + -f, --force Do not prompt for confirmation. Ignored when using --confirm + -H, --header strings custom headers. i.e. --header "Accept: value, AnotherHeader: myvalue" + --includeAll Include all results by iterating through each page + -k, --insecure Allow insecure server connections when using SSL + -l, --logMessage string Add custom message to the activity log + --maxJobs int Maximum number of jobs. 0 = unlimited (use with caution!) + --noAccept Ignore Accept header will remove the Accept header from requests, however PUT and POST requests will only see the effect + --noCache Force disabling of cached responses (overwrites cache setting) + -M, --noColor Don't use colors when displaying log entries on the console + --noLog Disables the activity log for the current command + --noProgress Disable progress bars + --noProxy Ignore the proxy settings + -n, --nullInput Don't read the input (stdin). Useful if using in shell for/while loops + -o, --output string Output format i.e. table, json, csv, csvheader (default "table") + --outputFile string Save JSON output to file (after select/view) + --outputFileRaw string Save raw response to file (before select/view) + -p, --pageSize int Maximum results per page (default 5) + --progress Show progress bar. This will also disable any other verbose output + --proxy string Proxy setting, i.e. http://10.0.0.1:8080 + -r, --raw Show raw response. This mode will force output=json and view=off + --select stringArray Comma separated list of properties to return. wildcards and globstar accepted, i.e. --select 'id,name,type,**.serialNumber' + --session string Session configuration + -P, --sessionPassword string Override session password + -U, --sessionUsername string Override session username. i.e. peter or t1234/peter (with tenant) + --silentExit Silent status codes do not affect the exit code + --silentStatusCodes string Status codes which will not print out an error message + --timeout string Request timeout duration, i.e. 60s, 2m (default "60s") + --totalPages int Total number of pages to get + -v, --verbose Verbose logging + --view string Use views when displaying data on the terminal. Disable using --view off (default "auto") + --withError Errors will be printed on stdout instead of stderr + --withTotalElements Request Cumulocity to include the total elements in the response statistics under .statistics.totalElements (introduced in 10.13) + -t, --withTotalPages Request Cumulocity to include the total pages in the response statistics under .statistics.totalPages + --workers int Number of workers (default 1) +``` + diff --git a/docs/go-c8y-cli/docs/cli/c8y/extensions/c8y_extensions_list.md b/docs/go-c8y-cli/docs/cli/c8y/extensions/c8y_extensions_list.md new file mode 100644 index 000000000..56059fea6 --- /dev/null +++ b/docs/go-c8y-cli/docs/cli/c8y/extensions/c8y_extensions_list.md @@ -0,0 +1,73 @@ +--- +category: extensions +title: c8y extensions list +--- +List installed extension commands + +``` +c8y extensions list [flags] +``` + +### Options + +``` + -h, --help help for list +``` + +### Options inherited from parent commands + +``` + --abortOnErrors int Abort batch when reaching specified number of errors (default 10) + --allowEmptyPipe Don't fail when piped input is empty (stdin) + --cache Enable cached responses + --cacheBodyPaths strings Cache should limit hashing of selected paths in the json body. Empty indicates all values + --cacheTTL string Cache time-to-live (TTL) as a duration, i.e. 60s, 2m (default "60s") + -c, --compact Compact instead of pretty-printed output when using json output. Pretty print is the default if output is the terminal + --confirm Prompt for confirmation + --confirmText string Custom confirmation text + --currentPage int Current page which should be returned + --customQueryParam strings add custom URL query parameters. i.e. --customQueryParam 'withCustomOption=true,myOtherOption=myvalue' + --debug Set very verbose log messages + --delay string delay after each request, i.e. 5ms, 1.2s (default "0ms") + --delayBefore string delay before each request, i.e. 5ms, 1.2s (default "0ms") + --dry Dry run. Don't send any data to the server + --dryFormat string Dry run output format. i.e. json, dump, markdown or curl (default "markdown") + --examples Show examples for the current command + --filter stringArray Apply a client side filter to response before returning it to the user + --flatten flatten json output by replacing nested json properties with properties where their names are represented by dot notation + -f, --force Do not prompt for confirmation. Ignored when using --confirm + -H, --header strings custom headers. i.e. --header "Accept: value, AnotherHeader: myvalue" + --includeAll Include all results by iterating through each page + -k, --insecure Allow insecure server connections when using SSL + -l, --logMessage string Add custom message to the activity log + --maxJobs int Maximum number of jobs. 0 = unlimited (use with caution!) + --noAccept Ignore Accept header will remove the Accept header from requests, however PUT and POST requests will only see the effect + --noCache Force disabling of cached responses (overwrites cache setting) + -M, --noColor Don't use colors when displaying log entries on the console + --noLog Disables the activity log for the current command + --noProgress Disable progress bars + --noProxy Ignore the proxy settings + -n, --nullInput Don't read the input (stdin). Useful if using in shell for/while loops + -o, --output string Output format i.e. table, json, csv, csvheader (default "table") + --outputFile string Save JSON output to file (after select/view) + --outputFileRaw string Save raw response to file (before select/view) + -p, --pageSize int Maximum results per page (default 5) + --progress Show progress bar. This will also disable any other verbose output + --proxy string Proxy setting, i.e. http://10.0.0.1:8080 + -r, --raw Show raw response. This mode will force output=json and view=off + --select stringArray Comma separated list of properties to return. wildcards and globstar accepted, i.e. --select 'id,name,type,**.serialNumber' + --session string Session configuration + -P, --sessionPassword string Override session password + -U, --sessionUsername string Override session username. i.e. peter or t1234/peter (with tenant) + --silentExit Silent status codes do not affect the exit code + --silentStatusCodes string Status codes which will not print out an error message + --timeout string Request timeout duration, i.e. 60s, 2m (default "60s") + --totalPages int Total number of pages to get + -v, --verbose Verbose logging + --view string Use views when displaying data on the terminal. Disable using --view off (default "auto") + --withError Errors will be printed on stdout instead of stderr + --withTotalElements Request Cumulocity to include the total elements in the response statistics under .statistics.totalElements (introduced in 10.13) + -t, --withTotalPages Request Cumulocity to include the total pages in the response statistics under .statistics.totalPages + --workers int Number of workers (default 1) +``` + diff --git a/docs/go-c8y-cli/docs/cli/c8y/extensions/c8y_extensions_update.md b/docs/go-c8y-cli/docs/cli/c8y/extensions/c8y_extensions_update.md new file mode 100644 index 000000000..afc20d82c --- /dev/null +++ b/docs/go-c8y-cli/docs/cli/c8y/extensions/c8y_extensions_update.md @@ -0,0 +1,74 @@ +--- +category: extensions +title: c8y extensions update +--- +Update installed extensions + +``` +c8y extensions update { | --all} [flags] +``` + +### Options + +``` + --all Update all extensions + -h, --help help for update +``` + +### Options inherited from parent commands + +``` + --abortOnErrors int Abort batch when reaching specified number of errors (default 10) + --allowEmptyPipe Don't fail when piped input is empty (stdin) + --cache Enable cached responses + --cacheBodyPaths strings Cache should limit hashing of selected paths in the json body. Empty indicates all values + --cacheTTL string Cache time-to-live (TTL) as a duration, i.e. 60s, 2m (default "60s") + -c, --compact Compact instead of pretty-printed output when using json output. Pretty print is the default if output is the terminal + --confirm Prompt for confirmation + --confirmText string Custom confirmation text + --currentPage int Current page which should be returned + --customQueryParam strings add custom URL query parameters. i.e. --customQueryParam 'withCustomOption=true,myOtherOption=myvalue' + --debug Set very verbose log messages + --delay string delay after each request, i.e. 5ms, 1.2s (default "0ms") + --delayBefore string delay before each request, i.e. 5ms, 1.2s (default "0ms") + --dry Dry run. Don't send any data to the server + --dryFormat string Dry run output format. i.e. json, dump, markdown or curl (default "markdown") + --examples Show examples for the current command + --filter stringArray Apply a client side filter to response before returning it to the user + --flatten flatten json output by replacing nested json properties with properties where their names are represented by dot notation + -f, --force Do not prompt for confirmation. Ignored when using --confirm + -H, --header strings custom headers. i.e. --header "Accept: value, AnotherHeader: myvalue" + --includeAll Include all results by iterating through each page + -k, --insecure Allow insecure server connections when using SSL + -l, --logMessage string Add custom message to the activity log + --maxJobs int Maximum number of jobs. 0 = unlimited (use with caution!) + --noAccept Ignore Accept header will remove the Accept header from requests, however PUT and POST requests will only see the effect + --noCache Force disabling of cached responses (overwrites cache setting) + -M, --noColor Don't use colors when displaying log entries on the console + --noLog Disables the activity log for the current command + --noProgress Disable progress bars + --noProxy Ignore the proxy settings + -n, --nullInput Don't read the input (stdin). Useful if using in shell for/while loops + -o, --output string Output format i.e. table, json, csv, csvheader (default "table") + --outputFile string Save JSON output to file (after select/view) + --outputFileRaw string Save raw response to file (before select/view) + -p, --pageSize int Maximum results per page (default 5) + --progress Show progress bar. This will also disable any other verbose output + --proxy string Proxy setting, i.e. http://10.0.0.1:8080 + -r, --raw Show raw response. This mode will force output=json and view=off + --select stringArray Comma separated list of properties to return. wildcards and globstar accepted, i.e. --select 'id,name,type,**.serialNumber' + --session string Session configuration + -P, --sessionPassword string Override session password + -U, --sessionUsername string Override session username. i.e. peter or t1234/peter (with tenant) + --silentExit Silent status codes do not affect the exit code + --silentStatusCodes string Status codes which will not print out an error message + --timeout string Request timeout duration, i.e. 60s, 2m (default "60s") + --totalPages int Total number of pages to get + -v, --verbose Verbose logging + --view string Use views when displaying data on the terminal. Disable using --view off (default "auto") + --withError Errors will be printed on stdout instead of stderr + --withTotalElements Request Cumulocity to include the total elements in the response statistics under .statistics.totalElements (introduced in 10.13) + -t, --withTotalPages Request Cumulocity to include the total pages in the response statistics under .statistics.totalPages + --workers int Number of workers (default 1) +``` + diff --git a/docs/go-c8y-cli/docs/cli/psc8y/Misc/ConvertTo-NestedJson.md b/docs/go-c8y-cli/docs/cli/psc8y/Misc/ConvertTo-NestedJson.md index fda27ce48..ea52c4855 100644 --- a/docs/go-c8y-cli/docs/cli/psc8y/Misc/ConvertTo-NestedJson.md +++ b/docs/go-c8y-cli/docs/cli/psc8y/Misc/ConvertTo-NestedJson.md @@ -91,7 +91,7 @@ This cmdlet supports the common parameters: -Debug, -ErrorAction, -ErrorVariable ### System.Object ## OUTPUTS -### System.Object +### System.String ## NOTES ## RELATED LINKS diff --git a/docs/go-c8y-cli/docs/cli/psc8y/Misc/Test-Json.md b/docs/go-c8y-cli/docs/cli/psc8y/Misc/Test-Json.md index b105daed4..24140da41 100644 --- a/docs/go-c8y-cli/docs/cli/psc8y/Misc/Test-Json.md +++ b/docs/go-c8y-cli/docs/cli/psc8y/Misc/Test-Json.md @@ -62,7 +62,7 @@ This cmdlet supports the common parameters: -Debug, -ErrorAction, -ErrorVariable ## OUTPUTS ### System.Boolean -### System.Object +### System.Boolean ## NOTES ## RELATED LINKS diff --git a/docs/go-c8y-cli/docs/concepts/extensions/01-overview.md b/docs/go-c8y-cli/docs/concepts/extensions/01-overview.md new file mode 100644 index 000000000..6023dee01 --- /dev/null +++ b/docs/go-c8y-cli/docs/concepts/extensions/01-overview.md @@ -0,0 +1,341 @@ +--- +title: Overview +--- + +import CodeExample from '@site/src/components/CodeExample'; + +## Overview + +Extensions allow you to customize go-c8y-cli to optimize you and your team's workflows. You can customize how your data is displayed and provide custom commands to simplify repetitive tasks. + +Extensions utilize existing go-c8y-cli features and packages them so they are easy to install and share. This makes it easy for a team to collaborate to add new commands for custom microservices, or just add custom columns to the device view so you can display new custom managed object fragments by default. + +By default extensions are accessible across all sessions. Though for scenarios where you would like to limit an extension to a single session or a group of sessions you can change the folder where the extensions are stored based on customer or some other task orientated grouping. + +## What is an extension? + +An extension is a git repository which contains multiple files/folders which control what the extension has to offer. The extension creator is free to pick which elements should be included. By default all extensions start with `c8y-`. This is to make public extension easier to discover, however the `c8y-` prefix will be ignore when using the extension (to avoid unnecessary typing). + +The different elements of an extension and where they are stored within the extension repository are listed below: + +| Type | Path | Description | +|-----------|------|-------------| +| Aliases | `extension.yaml` | Convenience commands which are easily accessible under the root command. e.g. `c8y my-alias` | +| API | `api/` | Commands which are generated via an API specification which re-use the same go-c8y-cli logic for in-built commands | +| Commands | `commands/` | More complex commands which can be written in language (e.g. bash, python etc.). The commands can call other go-c8y-cli commands or any other tooling | +| Templates | `templates/` | Any go-c8y-cli templates that can be referenced by when using the `template` flag | +| Views | `views/` | Any go-c8y-cli view definitions that control which fragments are shown for which items, e.g. show custom fragments for specific device types etc. | + +Below shows an example extension `c8y-myext` and a tree representation of the files associated with it. + +``` +c8y-myext/ +│ +├── extension.yaml +│ +├── api/ +│ ├── devices.yaml +│ └── datapoints.yaml +│ +├── commands/ +│ ├── do-something +│ └── mysubcmd/ +│ ├── list +│ └── get +│ +├── templates/ +│ ├── devices.json +│ └── applications.json +│ +└── views/ + ├── devices.json + └── applications.json +``` + +## Extension contents and examples + +### Aliases + +Aliases are defined in the `extension.yaml` file on the root level of the repository. The user can define any number of aliases, however aliases do not have any hierarchy so they should be reserved for commands which are frequently used and are a "time saver". + +The aliases should not clash with any existing commands. If the alias is too specific then it might be better to leave the alias out and allow the users to specify their own session-based aliases using `c8y alias set`. + +Read the [Aliases concept](/docs/configuration/aliases/) page for more details about it. + +Below is an example of an `extension.yaml` file which defines one alias called `mo`. `mo` pretty prints a managed object as json when given a managed object's id. + +```yaml title="file: extension.yaml" +version: "v1" +aliases: + - name: mo + description: Pretty print inventory managed object + command: | + inventory get --id "$1" --output json --view off + shell: false +``` + +Once the extension is installed, the above alias is accessible using: + + + +```bash +c8y mo 12345 +``` + + + +### API based commands + +Extensions support an API based approach where a specification is used to define commands in an extension. The specification is provided in the form of a YAML file which details a list of commands to be included. Multiple API specifications can be provided where each file represents a command group with it's own child commands. All of the command groups are then placed under the extension name. + +Since API commands are interpreted by the go-c8y-cli parsing engine, the commands can offer the same look and feel as the in-built commands, so the following features are available: + +* pipeline support +* flag completion using in-built types +* flag completion using external commands calls +* help text + +The above features are not supported in script based commands. + +:::note Fun Fact +The API YAML specification uses a similar specification which is used by go-c8y-cli to generate the in-built commands. Therefore should an extension become so popular that it warrants including it as an in-built command, then the same specification file can be included very quickly. +::: + +A schema is available to help guide you through the large set of options. Below shows a short example of the API specification. + +```yaml title="file: ./api/devices.yaml" +# yaml-language-server: $schema=https://raw.githubusercontent.com/reubenmiller/go-c8y-cli/feat/extensions-manager/tools/schema/extensionCommands.json +--- +group: + name: devices + description: Manage devices + descriptionLong: | + Manage devices which allows you to create/update/get/delete devices using a convenient commands + Examples + c8y organizer devices list | c8y organizer devices update --name "otherName" + +commands: + - name : list + description: Get collection of devices + descriptionLong: | + Some more detailed instructions on how to use the command + It can also use come additional context. This command uses a the 'query-inventory' preset + so that you don't have to define all of the other flags yourself. + exampleList: + - command: c8y organizer devices list + description: List devices + preset: + type: query-inventory + options: + param: q + value: has(c8y_IsLinux) + extensions: + - name: model + type: string + format: c8y_Hardware.model eq '%s' + description: Filter by model + + - name: excludeAgents + type: booleanDefault + description: Exclude agents + value: 'not(has(com_cumulocity_model_Agent))' +``` + +### Script based commands + +Script based commands allow you run custom logic inside a script. The script are organized as subcommands under the extension based on the folder hierarchy. It allows you to call any additional third-party commands to create more complex commands which can perform a sequence of steps. + +For more information about script based commands checkout the dedicated [script base commands](/docs/concepts/extensions/script-based-commands/) section. + +:::caution +Script based commands are less portable than API based commands, so try to avoid them if possible. +::: + +### Templates + +An extension can provide templates which are accessible when using the `template` flag. Templates can be more useful than commands when using them together with the template `var("name")` syntax which enables the user to customize the template values via the `templateVars` flag (which is tab completed). + +Information about what a template is and how to create on can be found in the [Templates concept](/docs/concepts/templates/) page. + +Below shows an small example of a `jsonnet` template to create a custom operation which accepts one template variable called `action`. + +```jsonnet title="file: ./templates/custom.operation.jsonnet" +{ + "description": "Execute custom operation", + "parameters": { + "action": var("action", "init 6"), + }, +} +``` + +The template is accessible via the `template` flag and the template is prefixed with `::` (without the `c8y-` prefix). + + + +```bash +c8y operations create --device 12345 --template myext::custom.operation.jsonnet --templateVars action="do_something_less_destructive" +``` + + + + +### Views + +Views allow you to custom what fragments are displayed by default for specific responses. A view definition has a selection criteria which controls when the view is activated and which columns are displayed on the console. + +Checkout the [Views concept](/docs/concepts/views/) page for more details. + +Like templates, extension views are also prefixed with `::` (without the `c8y-` prefix) to avoid name clashes amongst extensions and any other user-created views. + +Views are generally automatically selected based on their activation criteria (and priority), but they can also be manually activated using the global `view` flag. + + + +```bash +c8y operations create --device 12345 --view myext::mydevice +``` + + + +--- + +## How to use extensions + +This section details how to interact with extensions. + +### List already installed extensions + +A list of the currently installed sessions can displayed using + + + +```bash +c8y extensions list + +# show details about the extensions +c8y extensions list --raw +``` + + + +### Installing an extension + +**Prerequisites** + +Installing extensions requires the `git` command to be already installed. When an extension is installed from an external source, git is used to clone the repository to your file system. If you are cloning a private repository, then it is up to you to provide the necessary credentials when prompted. `go-c8y-cli` does not handle any of the repository authentication. If you are having problems then try cloning the repository manually. + +If you do not have git on your machine, then you also install an extension from a local folder. + +Extensions can be installed from the following locations: + +* From a local folder +* From GitHub via `/` +* From another git repository hosting service + + + + +```bash +# From a local folder +c8y extensions install . + +# From GitHub +c8y extensions install reubenmiller/c8y-myext + +# From another git repository hosting service +c8y extensions install https://github.com/reubenmiller/c8y-myext.git +``` + + + +If the extension contains any commands, then they grouped under the extension name (without the `c8y-` prefix). + + + +```sh +c8y myext list +``` + + + + +:::caution +Never edit an extension directly from the `go-c8y-cli` extension folder as you will lose any unpublished changes if you call the `c8y extensions delete ` command! + +Instead clone the extension manually and install it using the filesystem path to the cloned repo. This way `go-c8y-cli` will only create a symlink to the folder, so deleting the extension will only remove the symlink and not the original folder. +::: + +### Deleting an extension + +An extension can be removed by using the following command + + + +```bash +c8y extensions delete my-extension +``` + + + +### Creating an extension + +To make it easier to create your own extensions, there is an in-built command which generates an extension with some examples. This is done using + + + +```bash +c8y extensions create myext +``` + + + +Then follow the on-screen instructions for using it. + +:::tip +Creating an extension requires `git` to already by installed. +::: + +:::note +The extension will be automatically prefixed with `c8y-` so that all extensions follow the same convention making them easier to find on GitHub. +::: + +### Updating an extension + +Extensions can be updated using the following command. Any extensions which were installed from a local folder will be ignored. + + + +```bash +# Update a single extension +c8y extensions update myext + +# Update all extensions +c8y extensions update --all +``` + + + +Extensions are updated by `go-c8y-cli` by using standard git commands, `git fetch` and `git pull`. + +## Advanced usage + +This sections shows some more advanced use-cases. It is not intended for everyone, however you may find it useful in select scenarios. + +### Set custom extensions location for a specific c8y session + +If you want to isolate which extensions are used for a specific session or a group of sessions, then you can change the setting which controls where `go-c8y-cli` looks for extensions. + + + +```bash +# 1. change to the session you want to change (if you have not already done this) +set-session + +# 2. change the extension location +c8y settings update extensions.datadir "$HOME/my_customer/extensions/" + +# 3. install the extension +c8y extensions install reubenmiller/c8y-example +``` + + diff --git a/docs/go-c8y-cli/docs/concepts/extensions/02-api-commands/01-command-groups.md b/docs/go-c8y-cli/docs/concepts/extensions/02-api-commands/01-command-groups.md new file mode 100644 index 000000000..f4559c00f --- /dev/null +++ b/docs/go-c8y-cli/docs/concepts/extensions/02-api-commands/01-command-groups.md @@ -0,0 +1,78 @@ +--- +category: Concepts - Extensions - API based commands +title: Command groups +--- + +A command group is used to logically group commands of a similar nature together. + +For example the following in-built commands related to devices is grouped together under a command group called `devices`. + +```sh +c8y devices list +c8y devices create +c8y devices update +c8y devices delete +c8y devices get +``` + +All commands within one API specification are grouped together under the same subcommand. The name of the command group is defined inside the API specification. + +:::tip +It is advised to keep the name of the specification file the same as the group name. This makes it easier to maintain the plugin and find the corresponding spec files based on the command group name. +::: + +### Example: Multiple commands + +The example below defines a command group called `unicorns` and it has three commands. For simplicity all three commands just send a GET request to hardcoded endpoints (including both path and query parameters). + +```yaml title="file: api/unicorns.yaml" +# yaml-language-server: $schema=https://raw.githubusercontent.com/reubenmiller/go-c8y-cli/feat/extensions-manager/tools/schema/extensionCommands.json +--- +group: + name: unicorns + description: Fetch different unicorns + +commands: + # Sub command + - name: get-command1 + description: Get command 1 + method: GET + path: inventory/managedObjects?type=pink + + # Sub command + - name: get-command2 + description: Get command 2 + method: GET + path: inventory/managedObjects?type=yellow + + # Sub command + - name: get-command3 + description: Get command 3 + method: GET + path: inventory/managedObjects?type=blue +``` + +The commands are then callable under the extension which they below to, for example assuming it is under an extension called `myextension`, then you can list the available commands using the global `--help` flag: + + + +```sh +c8y myextension unicorns --help +``` + + + +```bash title="Output" +Example commands + +Usage: + c8y myextension unicorns [command] + +Available Commands: + get-command1 Get command 1 + get-command2 Get command 2 + get-command3 Get command 3 + +Flags: + -h, --help help for example +``` diff --git a/docs/go-c8y-cli/docs/concepts/extensions/02-api-commands/02-body-buliding.md b/docs/go-c8y-cli/docs/concepts/extensions/02-api-commands/02-body-buliding.md new file mode 100644 index 000000000..6a39446a8 --- /dev/null +++ b/docs/go-c8y-cli/docs/concepts/extensions/02-api-commands/02-body-buliding.md @@ -0,0 +1,273 @@ +--- +category: Concepts - Extensions - API based commands +title: Body building +--- + +import CodeExample from '@site/src/components/CodeExample'; + +One major aspect of an API is the creation or modification of entities. Such entities could be any thing such as devices, applications, configuration etc. + +API based commands provide an easy way for users to create the json bodies required for `PUT` and `POST` HTTP requests. The bodies can constructed by combining any of the following: + + +* Custom Flags +* Static templates (defined in the spec) +* Global `--template` flag +* Global `--data` flag + + +### Best practices + +Some general pointers to keep in mind when generating the body via the commands line: + +* Allow users to use `data` and `template` to create bodies (this gives them maximum flexibility) +* Keep number of flags below 10 (people generally like going through too many options) +* Provide templates if a request requires many fields or a very complex/nested data structure + + +## JSON Bodies + +By default the body of `PUT` and `POST` requests will use a json format. + +Below shows an api command which is used to provide a convenient way for users to create a new device using some custom fragments. + +```yaml +commands: + - name: create + description: Create device + method: POST + path: inventory/managedObjects + body: + - name: name + type: string + description: Device name + required: true + pipeline: true + + - name: type + type: string + property: type + description: Device type + validationSet: + - linux + - windows + - macos + + - name: interval + type: integer + property: c8y_RequiredAvailability.responseInterval + description: Response interval (for availability monitoring) + + - name: option1 + type: boolean + description: Add static value if the flag is used. If not used, then nothing will be added + value: customValue1 + + - name: agent + type: optional_fragment + property: com_cumulocity_model_Agent + description: Add special c8y agent fragment + + + bodyTemplates: + - type: jsonnet + template: | + {c8y_IsDevice:{}} +``` + + + +```sh +c8y organizer assets create --name foo --type macos --dry --interval 10 --agent --option1 +``` + + + + +````bash title="Output (dry)" +What If: Sending [POST] request to [https://{host}/inventory/managedObjects] + +### POST /inventory/managedObjects + +| header | value +|-------------------|--------------------------- +| Accept | application/json +| Authorization | Bearer {token} +| Content-Type | application/json + +#### Body + +```json +{ + "c8y_IsDevice": {}, + "c8y_RequiredAvailability": { + "responseInterval": 10 + }, + "com_cumulocity_model_Agent": {}, + "name": "foo", + "option1": "customValue1", + "type": "macos" +} +``` +```` + +## Binary + +### Uploading a file (with meta information) + +This kind of upload is called a `multipart/form-data` request. The request is generally used for uploading both binary files in addition to extra meta information describing the binary being uploaded. + +It is typically used in Cumulocity IoT to add new inventory binaries or application binaries to the platform. + +The file upload scenario can be utilized by using the special `file` type in the body section. The snippet below adds a `utils` group command with a single command called `upload`. + +```yaml title="file: api/utils.yaml" +# yaml-language-server: $schema=https://raw.githubusercontent.com/reubenmiller/go-c8y-cli/feat/extensions-manager/tools/schema/extensionCommands.json +--- +group: + name: utils + description: Example commands + +commands: + - name: upload + description: Upload a file to the inventory man + exampleList: + - command: c8y examples utils upload --file ./myfile.txt + description: Upload a single file + method: POST + path: inventory/binaries + body: + - name: file + type: file # <== the 'file' type activates the multipart form-data upload + description: File + + - name: name + type: string + description: Set the name of the binary file. This will be the name of the file when it is downloaded in the UI + + - name: type + type: string + required: false + description: Custom type. If left blank, the MIME type will be detected from the file extension +``` + +Assuming the api command was added to an extension called `examples`, the command can be executed to upload a file using: + + + +```bash +c8y examples utils upload --file ./myfile.txt +``` + + + + +Additional meta information can also be added by using the `template` or `data` flags which are automatically added to all requests which support a body. + + + +```bash +c8y examples utils upload --file ./myfile.txt --template "{foo:{bar:'other data'}}" +``` + + + +Like any of the commands, the output of the command can be inspected by using the `dry` flag. + + +````markdown title="Output (dry)" +What If: Sending [POST] request to [https://{host}/inventory/binaries] + +### POST /inventory/binaries + +| header | value +|-------------------|--------------------------- +| Accept | application/json +| Authorization | Bearer {token} +| Content-Type | multipart/form-data; boundary=9c365f1a9d792bcad0b782b551bd2ef0e1d3d7382a1d5de600a2c77999f9 + +#### Body + +```text +--9c365f1a9d792bcad0b782b551bd2ef0e1d3d7382a1d5de600a2c77999f9 +Content-Disposition: form-data; name="file"; filename="myfile.txt" +Content-Type: application/octet-stream + +testme + +--9c365f1a9d792bcad0b782b551bd2ef0e1d3d7382a1d5de600a2c77999f9 +Content-Disposition: form-data; name="object" + +{ + "foo": { + "bar": "other data" + }, + "name": "myfile.txt", + "type": "text/plain; charset=utf-8" +} + +--9c365f1a9d792bcad0b782b551bd2ef0e1d3d7382a1d5de600a2c77999f9-- + +``` +```` + +### Uploading binary data + +Plain binary data can be sent in a request using the `fileContents` type. Using this type within the `body` section will send the raw file contents in the body. In contrast to the `file` type, it will not send the request as a multipart form-data request. + +To demonstrate this, a new command can be added called `replace` which sends a PUT request to the inventory binary API to replace an existing binary with the contents from a new file. The api command definition to achieve this is shown below: + +```yaml title="file: api/utils.yaml" +# yaml-language-server: $schema=https://raw.githubusercontent.com/reubenmiller/go-c8y-cli/feat/extensions-manager/tools/schema/extensionCommands.json +--- +group: + name: utils + description: Example commands + +commands: + # ... previous command (omitted for simplicity) + + - name: replace + description: Replace the contents of a binary file with new contents + method: PUT + path: inventory/binaries/{id} + pathParameters: + - name: id + type: string + description: Existing binary id + body: + - name: file + type: fileContents # <== Just upload bytes + description: file +``` + +This command will require us to know the id of the existing binary that we wish to replace, however it should be easy enough to find using the other `go-c8y-cli` command (e.g. `c8y binaries list`). + + + +```bash +c8y examples utils replace --id 12345 --file ./myfile.txt --dry +``` + + + +Using `dry` shows how the request body is different to the previous example which used a multipart form-data upload. + +````bash title="Output (dry)" +What If: Sending [PUT] request to [https://{host}/inventory/binaries/12345] + +### POST /inventory/binaries + +| header | value +|-------------------|--------------------------- +| Accept | application/json +| Authorization | Bearer {token} +| Content-Type | application/json + +#### Body + +```text +testme + +``` +```` diff --git a/docs/go-c8y-cli/docs/concepts/extensions/02-api-commands/03-tab-completion.md b/docs/go-c8y-cli/docs/concepts/extensions/02-api-commands/03-tab-completion.md new file mode 100644 index 000000000..061c79c12 --- /dev/null +++ b/docs/go-c8y-cli/docs/concepts/extensions/02-api-commands/03-tab-completion.md @@ -0,0 +1,103 @@ +--- +category: Concepts - Extensions - API based commands +title: Tab completion +--- + +Tab completion is the killer feature on the commands line. It improves the useability of the extension and can reduce the reliance on documentation (though it shouldn't be a substitute for good docs :wink:) + +The following sections detail the different tab completion mechanisms available for use. + +## Tab completion with simple statics lists + +If a flag has a fixed number of allowed values, then the `validationSet` option is the perfect fit. The options will be presented to the user when they try tab completion when using the flag. + +For example, Cumulocity IoT's operation endpoint supports filtering the type of operations by status, and the states should be one of; `PENDING`, `EXECUTING`, `FAILED` or `SUCCESSFUL`. So using this knowledge a flag can be added to the `list` command which sets the `status` query parameter, and the users can use tab completion to check which options are available for usage. Below shows a snippet of the commands: + +```yaml +commands: + - name: list + description: Get collection of operations + method: GET + path: devicecontrol/operations + queryParameters: + - name: status + type: string + description: Filter by status + validationSet: + - PENDING + - EXECUTING + - FAILED + - SUCCESSFUL +``` + +Example usage + + + +```sh +c8y organizer assets list --status +``` + + + +```bash title="Output" +PENDING +EXECUTING +FAILED +SUCCESSFUL +``` + +## Tab completion using shell commands + +The external tab completion is not just limited to `c8y` commands, you can also use a shell to call any commands you would like. + +:::warning +If the extensions uses a shell to execute the completion command then it might make your extension less portable as Windows users might not have access to a bash shell. + +Instead try to use the `c8y` command instead of calling a shell if possible. +::: + +The command snippet below shows an example of a `create` command which accepts a type flag when building the request body. The type flag has a tab completion command which queries existing devices in the tenant, and returns a unique list of device types (only based on the first 2000 devices found). + +```yaml +commands: + - name: create + description: Create managed object + method: POST + path: inventory/managedObjects + body: + - name: type + type: string + property: type + description: Device type + completion: + type: external + command: + - /bin/bash + - -c + - c8y devices list --pageSize 2000 --select type,type --output completion | sort | uniq +``` + +The above command will provide the following experience when the user tries to TAB completion the value provided to the `--type` flag. + + + +```sh +c8y organizer assets create --type +``` + + + +```bash title="Output" +Connex Spot Monitor -- type: Connex Spot Monitor +PMP -- type: PMP +RV700 -- type: RV700 +c8y_OPCUA_Device_Agent -- type: c8y_OPCUA_Device_Agent +c8y_SNMP -- type: c8y_SNMP +c8y_dm_example_device -- type: c8y_dm_example_device +c8y_lwm2m_connector_device -- type: c8y_lwm2m_connector_device +``` + +:::note +The `--select type,type` part of the completion command is not a typo. By included more than one columns of data means that the the other colums will be used in the description of the option. In zsh it makes the list a bit more readable. +::: diff --git a/docs/go-c8y-cli/docs/concepts/extensions/02-api-commands/04-parameter-types.md b/docs/go-c8y-cli/docs/concepts/extensions/02-api-commands/04-parameter-types.md new file mode 100644 index 000000000..1a2dfdfd8 --- /dev/null +++ b/docs/go-c8y-cli/docs/concepts/extensions/02-api-commands/04-parameter-types.md @@ -0,0 +1,222 @@ +--- +category: Concepts - Extensions - API based commands +title: Parameter types +--- + +Parameter (or flag) types are used to defined how a flag's value should be interpreted. The supported parameter types which can be referenced from the API specification are listed below. + +## Introduction + +Flags can be added to a command by placing a flag definition under any of the following sections: + +* `pathParameters` +* `headerParameters` +* `queryParameters` +* `body` + +A flag definition provides documentation about the flag, and how the flag should be interpreted. The definition not only contains how the value should be interpreted but also where the value should be written to when the API request is being generated. + +For example, a minimal flag definition under the `queryParameters` section could look like this: + +```yaml +queryParameters: +- name: name + type: string +``` + +The following table shows how the parameter would be translated to a query parameter used in the outgoing HTTP request. + +|Flag|Translated QueryParameter| +|----|----| +|`--name example`|`?name=example`| + +Sometimes the name of the flag might be different to the corresponding query parameter name. This is an important usability aspect, as commands can provide a contextualized abstraction on top of the REST API, so a flag's meaning can be slighly different to the API's meaning. In this case a custom mapping can be provided by using the `property` field. + +Extending the previous snippet, the `name` flag can be changed to write to write to the `fragment` query parameter instead of the `name`. + +```yaml +queryParameters: +- name: name + type: string + property: fragment +``` + +Now using the value provided via the `--name` flag will be assigned to the `fragment` query parameter. + +|Flag|QueryParameter| +|----|----| +|`--name example`|`?fragment=example`| + +The same mapping principle can be applied to any of the other parameters (e.g. body, pathParameters, headerParameters etc.). + + +## Basic types + +The following basic types are available for use. + +### Boolean + +|Type|Description|Example usage|Example output| +|----|----|----|----| +|`boolean`|Boolean value| `--enable` |`true` or `false`| +|`booleanDefault`|Boolean value with default| `--enable` |`true` or `false` (depending on the `.default` value)| +|`optional_fragment`|Add optional fragment (empty json object)| `--enable` |`{}`| + +### Date / Time + +|Type|Description|Example usage|Example output| +|----|----|----|----| +|`datetime`|Relative or fixed date/time string| `--dateFrom -10d` |`"2023-04-27T22:52:06.622+02:00"`| +|`date`|Relative or fixed time string| `--dateFrom -10d` |`"2023-04-27"`| + +### Numbers (integer/float) + +|Type|Description|Example usage|Example output| +|----|----|----|----| +|`integer`|Integer value| `--value 42` |`42`| +|`float`|Float value| `--value 42.1` |`42.1`| + +### String + +|Type|Description|Example usage|Example output| +|----|----|----|----| +|`string`|String value| `--item "text value"` |`"text value"`| +|`stringStatic`|Fixed string which is always added (set by `.value`)|N/A|`"foobar"`| +|`string[]`|List of strings|`--item one --item two`|`["one", "two"]`| +|`stringcsv[]`|List of strings as csv list|`--item one --item two`|`"one,two"`| + + +### File based + +|Type|Description|Example usage|Example output| +|----|----|----|----| +|`file`|File upload with optional meta data (Multipart FormData request)| `--file ./foobar.txt` |`<>`| +|`fileContents`|File contents (for binary uploads)| `--file ./foobar.txt` |`<>`| +|`fileContentsAsString`|File contents as a string (for usage in a json body)| `--file ./foobar.txt` |`"file":"<>"`| +|`attachment`|File upload without optional meta data (Multipart FormData request)| `--file ./foobar.txt` |`<>`| +|`binaryUploadURL`|Upload file as Inventory binary and return the URL| `--file ./foobar.txt` |`"https://{host}/inventory/binaries/12345"`| + + +### JSON + +|Type|Description|Example usage|Example output| +|----|----|----|----| +|`json_custom`|JSON shorthand (or json string)| `--mydata "foo.bar=true"` |`{"mydata":{"foo":{"bar":true}}}`| + + +## Cumulocity specific types + +This section contains Cumulocity IoT specific types which most involve both tab completion and/or lookups depending on the exact type. + +### Applications + +|Type|Description|Example usage|Example output| +|----|----|----|----| +|`application`|Application|`--application administration`|`"12"`| +|`applicationname`|Application name|`--application cockpit`|`"cockpit"`| +|`hostedapplication`|Hosted application (e.g. type=`HOSTED`)|`--application devicemanagement`|`"12"`| +|`microservice`|Microservice (type=`MICROSERVICE`)|`--microservice report-agent`|`"8"`| +|`microservicename`|Microservice name|`--microservice report-agent`|`"report-agent"`| +|`microserviceinstance`|Microservice instance (completion only) (use `.dependsOn` field to specify related microservice|`--id advanced-software-mgmt --instance `|`"advanced-software-mgmt-scope-management-deployment-7597ddb65lj6"`| + +### Devices / Agents / Sources + +|Type|Description|Example usage|Example output| +|----|----|----|----| +|`source`|Source (e.g. event, alarm, measurement)| `--source 12345` |`"12345"`| +|`id[]`|List of ids| `--id 1 --id 2` |`"12345"`| +|`agent[]`|List of agents| `--agent 1 --id 2` |`"1", "2"`| +|`device[]`|List of devices| `--device 1 --id 2` |`"1", "2"`| + +### Device Groups + +|Type|Description|Example usage|Example output| +|----|----|----|----| +|`devicegroup[]`|Device group| `--group "germany"` |`"12345"`| +|`smartgroup[]`|Smart group| `--group "australia"` |`"12345"`| + + +### Device management + +|Type|Description|Example usage|Example output| +|----|----|----|----| +|`deviceservice[]`|Device service| `--device 12345 --service my` (use `.dependsOn` to set which flag provides the device) to be set |`"abcdefg"`| + +### Device requests + +|Type|Description|Example usage|Example output| +|----|----|----|----| +|`devicerequest`|Device request|`--id abcdef`|`"abcdef"`| +|`devicerequest[]`|Device request array|`--id abcdef`|`["abcdef"]`| + + +### Misc. + +|Type|Description|Example usage|Example output| +|----|----|----|----| +|`certificate[]`|Trusted device certificate| `--cert 4fd8df0378f2cafd5e322c1aaa8b87300704e9a5` |`"4fd8df0378f2cafd5e322c1aaa8b87300704e9a5"`| +|`certificatefile`|Certificate file| `--certfile ./my.cert` |`<>`| + + +### Notifications2 + +|Type|Description|Example usage|Example output| +|----|----|----|----| +|`subscriptionId`|Subscription id|`--id abcdef`|`"abcdef"`| +|`subscriptionName`|Subscription name|`--name abcdef`|`"abcdef"`| + + +### Repository + +#### Configuration +|Type|Description|Depends On|Example usage|Example output| +|----|----|----|----|----| +|`configuration[]`|Configuration repository item| - | `--config linux_conf` |`"12345"`| +|`configurationDetails`|Configuration repository item details| - | `--config linux_conf` |`{"configuration": {"name": "example-config","type": "agentConfig","url": "https://test.com/content/raw/app.json"}}`| + +#### Device profile + +|Type|Description|Depends On|Example usage|Example output| +|----|----|----|----|----| +|`deviceprofile[]`|Device profile| - | `--profile bundle-deployment` |`"12345"`| + + +#### Firmware + +|Type|Description|Depends On|Example usage|Example output| +|----|----|----|----|----| +|`firmware[]`|Firmware repository item| - | `--firmware ubuntu-22_04` |`"12345"`| +|`firmwarename`|Firmware name| - | `--firmware ubuntu-22_04` |`"ubuntu-22_04"`| +|`firmwareversion[]`|Firmware version| `firmware` | `--version 1.0.0` |`"12345"`| +|`firmwareversionName`|Firmware version name| `firmware` | `--version 2.0.0` |`"2.0.0"`| +|`firmwarepatch[]`|Firmware patch| `firmware` | `--patch 1.0.1` |`"12345"`| +|`firmwarepatchName`|Firmware patch name| `firmware` | `--patch 1.0.1` |`"1.0.1"`| +|`firmwareDetails`|Firmware version details| `firmware` | `--firmware "ubuntu-22_04" --version "1.0.1"` |`{"version":{"name":"ubuntu-22_04","version":"1.0.1","url":"https://example.com"}}`| + +#### Software + +|Type|Description|Depends On|Example usage|Example output| +|----|----|----|----|----| +|`software[]`|Software| - | `--software vim` |`"12345"`| +|`softwareName`|Software name| - | `--software vim` |`"vim"`| +|`softwareDetails`|Software version details| `software` | `--software vim --version ""` |`"12345"`| +|`softwareversion[]`|Software version| `software` | `--software vim --version 1.0.0` |`"98765"`| +|`softwareversionName`|Software version name| `software` | `--software vim --version 1.0.0` |`"1.0.0"`| + +### Tenant + +|Type|Description|Example usage|Example output| +|----|----|----|----| +|`tenant`|Tenant id| `--tenant t12345` |`"t12345"`| +|`tenantname`|Tenant name| `--tenant mytenant` |`"mytenant"`| + + +### Users / User groups / Roles + +|Type|Description|Example usage|Example output| +|----|----|----|----| +|`role[]`|User role| `--role ROLE_ALARM_*` |`"ROLE_ALARM_READ"`| +|`roleself[]`|User role url| `--role ROLE_ALARM_*` |`"https://{host}/user/roles/ROLE_ALARM_READ"`| +|`user[]`|User id| `--user john*` |`"john.smith@example.com"`| +|`userself[]`|User self url| `--user john*` |`"https://{host}/user/{tenant}/users/john.smith@example.com"`| +|`usergroup[]`|User group| `--usergroup admins` |`"2"`| diff --git a/docs/go-c8y-cli/docs/concepts/extensions/02-api-commands/index.md b/docs/go-c8y-cli/docs/concepts/extensions/02-api-commands/index.md new file mode 100644 index 000000000..ec08952ba --- /dev/null +++ b/docs/go-c8y-cli/docs/concepts/extensions/02-api-commands/index.md @@ -0,0 +1,18 @@ +--- +category: Concepts - Extensions - API based commands +title: API based commands +--- + +import DocCardList from '@theme/DocCardList'; + +## Overview + +:::tip +A general rule of thumb is that try to use API based commands by default as they are more portable as they do not rely on any external executables, only go-c8y-cli. +::: + +API based commands are commands which are generated via a YAML specification. Each specification can contain multiple commands and each command corresponds to one HTTP REST request. + +API based commands offer a first class integration into go-c8y-cli as the feel much more like other in-built commands as they include; tab completion, documentation, support for in-built data types (e.g. devices, agents, applications etc.). This is possible because under the hood go-c8y-cli is applying the same API processing that it uses internally to generate the golang code for each command, however the main difference is that go-c8y-cli is interpreting the commands at runtime rather than generating static golang code. + + diff --git a/docs/go-c8y-cli/docs/concepts/extensions/03-script-based-commands.md b/docs/go-c8y-cli/docs/concepts/extensions/03-script-based-commands.md new file mode 100644 index 000000000..5916b911b --- /dev/null +++ b/docs/go-c8y-cli/docs/concepts/extensions/03-script-based-commands.md @@ -0,0 +1,333 @@ +--- +category: Concepts - Extensions +title: Script based commands +--- + +An extension can include any number of commands. The structure of the commands is based on the folder structure, so you can group commands by placing them under the same sub folder. There is no limit to the number of sub folders, however you should keep it under 4-5 levels so it is not annoying for users to type. + +Below shows some examples of commands provided by an extension called `c8y-myext` and how each command can be executed. + +| Path | Command called via | +|-----------|------| +| `./commands/services/list` | `c8y myext services list` | +| `./commands/services/get` | `c8y myext services get` | +| `./commands/list` | `c8y myext list` | + +The commands themselves can be written in any script-based language, e.g. `bash`, `python`, `ruby` etc., however they should include a [Shebang](https://en.wikipedia.org/wiki/Shebang_(Unix)) so that the correct shell interpreter can be called by the operating system. + +Below is a simple example of a bash-based script. + +```bash title="file: ./commands/list" +#!/usr/bin/env bash +set -e +echo "Hey look at me" >&2 +``` + +:::note +Note: Make sure that your file is executable. On Unix, you can execute `chmod +x file_name` in the command line to make file_name executable. On Windows, you can run `git init -b main`, `git add file_name`, then `git update-index --chmod=+x file_name`. +::: + +## General Tips + +Below are some general tips to keep in mind when creating script based commands: + +* Explicitly check for any dependencies (don't assume the user has these installed) +* Prefer bash over any other shell. Bash is installed by default on many different Operating Systems (though python might be a close second) +* Support the `--help|-h` flag so users can check how to use the command (don't rely on online documentation) +* Do not use `sudo` inside any scripts. If a script is called without the required elevated rights, then just log an error saying so + + +## Bash Tips + +The following sections detail tips when writing bash scripts. Some of the topics can also be applied to other scripting languages, however the examples are all shown in bash. + +### Remove file extension + +The script should not have a file extension as the name of the file is used in go-c8y-cli to list which commands are available to use. + + +:::danger bad + +```bash +commands/myscript.sh +``` + +::: + +:::tip good + +``` +commands/myscript +``` + +::: + +### [Shebang](https://www.youtube.com/watch?v=dBUGfs9rwms) for better portability + +A [shebang](https://en.wikipedia.org/wiki/Shebang_(Unix)) is the first line of a script which indicates which interpreters (e.g. shell) should be used to run the script. + +It is recommended to use the `#!/usr/bin/env bash` shebang over `#!/bin/bash` as it allows users more control over which `bash` executable should be used if they have multiple bash interpreters installed. + +```bash +#!/usr/bin/env bash +``` + +:::info +MacOS has bash version 3 installed by default. For reference bash 3.2 was released in 2006, and the more modern bash version 5.0 was released in 2019. By using the `#!/usr/bin/env bash` shebang, MacOS users can install a more recent version of bash using [homebrew](https://brew.sh/) and the newer version of bash will be used by default (providing the `PATH` variable points to the homebrew bin path). +::: + + +### Fail on unexpected errors + +Use the `set -e` option to stop on unexpected errors. This is recommended because an unexpected errors can have some nasty side-effects as errors usually result in assigning an empty value to a variable, and downstream usage of the variable without validation could produce unexpected results (e.g. delete too many items etc.) + +```bash +#!/usr/bin/env bash +set -e + +echo "do something" >&2 +``` + + +### Print log messages to standard error + +All log messages, user information and or progress indicators should be written to standard error (not standard output). This is because only standard output is piped by default, and generally piping log messages to any downstream executable does not make any sense as the messages are intended for users and not binaries. + +You can use echo and the stream redirection syntax to write to standard error. Below shows an example of this. + +```bash +echo "Running script-based command: $0" >&2 +``` + +Alternatively you can create helper functions to log messages to standard error. The helper functions prepend the log level so that messages can be given different symantec meanings. + +```bash +# Log helpers +warn() { echo "WARN $*" >&2; } +error() { echo "ERROR $*" >&2; } + +# Use the helpers +warn "Something unexpected happened, but it's ok, we can still continue :)" +error "Oops, you did something wrong that we don't know how to fix" +``` + +```bash title="Output" +WARN Something unexpected happened, but it's ok, we can still continue :) +ERROR Oops, you did something wrong that we don't know how to fix +``` + +### Use shellcheck to validate your script during development + +[shellcheck](https://github.com/koalaman/shellcheck) is a great tool which checks for common mistakes/pitfalls. There are many IDE integrations available (e.g. VS Code extension etc.), so please use it, it will save you a world of pain, especially if you are not so experienced with writing bash scripts. + +### Argument parsing + +Support flag based argument parsing can be tricky in bash. You might be better off checking out some of the following projects to help you + +|Name|Description| +|---|---| +|[docopt.sh](https://github.com/andsens/docopt.sh)|Automatically add bash parsing to a script from a doc string (at build time) | + + +However if you want to do parsing yourself, then you can use the following boiler plate: + +```bash +#!/usr/bin/env bash +set -e + +# Help text +help() { + cat < Match by name + --onlyAgents Only include agents +EOF +} + + +NAME=${NAME:-} +AGENTS_ONLY=${AGENTS_ONLY:-"false"} +POSITIONAL_ARGS=() + +# +# Parse Flags: --flag , or boolean/switch flags: --help|-h +# +while [ $# -gt 0 ]; do + case "$1" in + # Flag which expects an argument, e.g. --name "value" + --name) + NAME="$2" + shift + ;; + + # Flag which does not expect an argument, e.g. --onlyAgents + --onlyAgents) + AGENTS_ONLY="true" + ;; + + # Support showing the help when users provide '-h' or '--help' + -h|--help) + help + exit 0 + ;; + + # Save positional arguments and restore them later on + *) + POSITIONAL_ARGS+=("$1") + ;; + esac + shift +done + +# Restore additional arguments which can then be referenced via "$@" and "$1" etc. +set -- "${POSITIONAL_ARGS[@]}" + +echo "do something: NAME=${NAME}, AGENTS_ONLY=${AGENTS_ONLY}, OTHER_OPTIONS=$*" +``` + +### Scripts should be executable + +A script based command must be made executable before go-c8y-cli can call it. The best place to do this is to make the script executable during development and commit the scripts to the git repository. + +On MacOS/Linux you can make all scripts under the `commands/` folder executable by using this one-liner. + +```bash +find commands/ -name "*" -exec chmod +x {} \; +``` + +Or if you are on Windows and are not using bash, then you can use the git command: + +```bash +git update-index --chmod=+x commands/mycommand +``` + +Afterwards you will have to commit any changes to the file (yes changing the execution bit on files trigger a git change by default). + + +### Don't use sudo + +Please don't use `sudo` inside any commands. If you need to run anything with elevated rights, log an error message indicating that the command needs to be run with elevated rights. + +## Examples + +### Example: List command with custom options + +In this example a custom command is created to list the devices or agents in the platform. The user is given the option to search for devices or agents via using the `--onlyAgents` flag. The devices or agents can be filtered by name via the `--name ` flag. + +A script based command is created by placing it under the `commands/` folder in the extension folder. You can create a command hierarchy by creating sub folder, for example `commands/devices/list` can be called via `c8y devices list`. + +The script below shows the contents of the `list` command which is placed under the `commands/devices/` folder of the extension. + +```bash title="file: commands/devices/list" +#!/usr/bin/env bash + +# Stop on unexpected errors +set -e + +# Logging helper functions +warn() { echo "WARN $*" >&2; } +error() { echo "ERROR $*" >&2; } + +# Help text +help() { + cat < Match by name + --onlyAgents Only include agents +EOF +} + + +NAME=${NAME:-} +AGENTS_ONLY=${AGENTS_ONLY:-"false"} +POSITIONAL_ARGS=() + +# +# Parse Flags: --flag , or boolean/switch flags: --help|-h +# +while [ $# -gt 0 ]; do + case "$1" in + # Flag which expects an argument, e.g. --name "value" + --name) + NAME="$2" + shift + ;; + + # Flag which does not expect an argument, e.g. --onlyAgents + --onlyAgents) + AGENTS_ONLY="true" + ;; + + # Support showing the help when users provide '-h' or '--help' + -h|--help) + help + exit 0 + ;; + + # Save positional arguments and restore them later on + *) + POSITIONAL_ARGS+=("$1") + ;; + esac + shift +done + +# Restore additional arguments which can then be referenced via "$@" and "$1" etc. +set -- "${POSITIONAL_ARGS[@]}" + +## Validate arguments + +# Check NAME property is not empty +if [ -z "$NAME" ]; then + error "name should not be empty" + help + exit 1 +fi + +# Finally do what we came here to do! Pass any additional positional args using "$@" syntax +if [ "$AGENTS_ONLY" = "true" ]; then + c8y agents list --name "$NAME" "$@" +else + c8y devices list --name "$NAME" "$@" +fi +``` + +Assuming the command was placed under an extension called `organizer`, the command can be called via: + + + +```sh +c8y organizer devices list --name "my device*" +``` + + + +The above script was built to pass extra flags/arguments provided by the user to the underlying `c8y devices|agents list` command, so we can provide the additional flags such as `--pageSize 100` will be passed to the other `c8y` commands. + + + +```sh +c8y organizer devices list --name "my device*" --pageSize 100 +``` + + + +:::note +go-c8y-cli has no way of knowing whether script based commands are support the extra flags or not, so tab completion will not be available. +::: diff --git a/docs/go-c8y-cli/docs/concepts/extensions/index.md b/docs/go-c8y-cli/docs/concepts/extensions/index.md new file mode 100644 index 000000000..82db4af6c --- /dev/null +++ b/docs/go-c8y-cli/docs/concepts/extensions/index.md @@ -0,0 +1,20 @@ +--- +category: Tutorials - Extensions +title: Extensions +--- + +import DocCardList from '@theme/DocCardList'; + +:::tip +Extensions are only supported from version 2.30.0 onwards. +::: + +## Overview + +Extensions allow you to customize go-c8y-cli to optimize you and your team's workflows. You can customize how your data is displayed and provide custom commands to simplify repetitive tasks. + +Extensions utilize existing go-c8y-cli features and packages them so they are easy to install and share. This makes it easy for a team to collaborate to add new commands for custom microservices, or just add custom columns to the device view so you can display new custom managed object fragments by default. + +By default extensions are accessible across all sessions. Though for scenarios where you would like to limit an extension to a single session or a group of sessions you can change the folder where the extensions are stored based on customer or some other task orientated grouping. + + diff --git a/docs/go-c8y-cli/docs/concepts/views.md b/docs/go-c8y-cli/docs/concepts/views.md index 0f7ab001d..b5f2db483 100644 --- a/docs/go-c8y-cli/docs/concepts/views.md +++ b/docs/go-c8y-cli/docs/concepts/views.md @@ -92,6 +92,8 @@ A view definition supports matching by the following criteria: * `self` - `.self` value of first entry (supports regex) * `type` - `.type` value of first entry (supports regex) * `fragments` - List of fragments (all fragments must exist) +* `requestPath` - Match the outgoing request PATH (supports regex) +* `requestMethod` - Match the outgoing request Method (e.g. GET, POST etc.) (supports regex) :::note The matching criteria can be combined to provide more precise matching. diff --git a/docs/go-c8y-cli/docs/tutorials/extending-cmdlets.md b/docs/go-c8y-cli/docs/tutorials/extending-cmdlets.md index ee054d0d4..c13d1905e 100644 --- a/docs/go-c8y-cli/docs/tutorials/extending-cmdlets.md +++ b/docs/go-c8y-cli/docs/tutorials/extending-cmdlets.md @@ -4,6 +4,12 @@ category: Tutorials - Powershell title: Extending cmdlets using scripts and Functions --- +:::tip +It is recommended to use [extensions](/docs/concepts/extensions/) instead of creating your own PowerShell module. Extensions are available in go-c8y-cli >= 2.30.0 and provide a more portable/native experience. They are just as easy to install and share, and can be used in any shell. + +Or just check out the [Tutorials](/docs/tutorials/extensions/). +::: + ## Example 1: Script to export data for specific devices ### Goal diff --git a/docs/go-c8y-cli/docs/tutorials/extending-powershell-module.md b/docs/go-c8y-cli/docs/tutorials/extending-powershell-module.md index 4c6ff6aef..69689f719 100644 --- a/docs/go-c8y-cli/docs/tutorials/extending-powershell-module.md +++ b/docs/go-c8y-cli/docs/tutorials/extending-powershell-module.md @@ -4,6 +4,12 @@ category: Tutorials - Powershell title: Extend using module --- +:::tip +It is recommended to use [extensions](/docs/concepts/extensions/) instead of creating your own PowerShell module. Extensions are available in go-c8y-cli >= 2.30.0 and provide a more portable/native experience. They are just as easy to install and share, and can be used in any shell. + +Or just check out the [Tutorials](/docs/tutorials/extensions/). +::: + An example how to extend the `PSc8y` PowerShell module using another PowerShell module is shown in the following demo project: [Example PSc8y.example Module](https://github.com/reubenmiller/PSc8y.example) diff --git a/docs/go-c8y-cli/docs/tutorials/extensions/creating-an-extension.md b/docs/go-c8y-cli/docs/tutorials/extensions/creating-an-extension.md new file mode 100644 index 000000000..75e3c0c09 --- /dev/null +++ b/docs/go-c8y-cli/docs/tutorials/extensions/creating-an-extension.md @@ -0,0 +1,594 @@ +--- +category: Tutorials - Extensions +title: Creating your own extension +--- + +import Tabs from '@theme/Tabs'; +import TabItem from '@theme/TabItem'; +import CodeExample from '@site/src/components/CodeExample'; + +:::tip +Extensions are only supported from version 2.30.0 onwards. +::: + +:::note +Extensions are a new feature and therefore the documentation could be a little under cooked. However I didn't want to delay it any more because it is a super useful feature. So if you notice something that is not quite right with the docs or the feature, please don't hesitate to [create an issue](https://github.com/reubenmiller/go-c8y-cli/issues/new), or a PR with the fix. + +Also, if you like the project please add a star to the [GitHub repository](https://github.com/reubenmiller/go-c8y-cli). +::: + +## Preface + +Extensions support an api which is not so dissimilar to the [OpenAPI Specification](https://swagger.io/specification/). It was by design that the command spec should not use OpenAPI spec as there is usually a useability layer on top of the API to make the command more user-friendly. + +In the future there might be tooling to create the command specs automatically from the open api specs, which would make it quicker to create or update go-c8y-cli extensions. + +## Example 1: Create an extension for a microservice + +Let's say that you have developed a microservice that is deployed in Cumulocity IoT, and now you would like to create some a CLI interface which can be used by users to get the most out of your new microservice. + +For this example, let's assume we have a microservice called `organizer` and it is responsible for the management of some generic IoT assets. + +|API|Description|Command (e.g. what we are going to create)| +|---|-----------|---------------| +|`GET` `/service/organizer/assets`| Get a list of the already existing plans | `c8y organizer assets list` | +|`PUT` `/service/organizer/assets/{id}`| Update an existing plan | `c8y organizer assets update --id "1234"` | +|`GET` `/service/organizer/assets/{id}`| Get a single plan by id | `c8y organizer assets get --id "1234"` | +|`POST` `/service/organizer/assets`| Create a new plan | `c8y organizer assets create --template ./sometemplate.jsonnet` | +|`DELETE` `/service/organizer/assets/{id}`| Delete an existing plan | `c8y organizer assets delete --id "1234"` | + + +:::note +Though with the power of `go-c8y-cli`, the idea is that we would like to provide a similar interface for the extension so it feels and behaves like any other go-c8y-cli command. So ideally you should try to align to it as much as possible, for example use `create` instead of `new` when creating a new object, and use `update` instead `set` or any other synonym. +::: + +In addition to the command we are going to provide some advanced functionality to make the commands more usable. We will also look at adding the following features: + +* Support pipeline (using the asset id as the pipeline iterator) +* Tab completion for the asset +* Supported named lookups of assets + +### Step 1: Creating the extension scaffolding + +A new extension can be either created manually (if you know the required structure), or you can use the following command which creates an extension: + + + +```sh +cd ~ +c8y extensions create organizer +``` + + + +:::note +You may have noticed that the extension will have the `c8y-` prefix. This is so that it is easier to find the extensions as extensions are intended to be shared amongst users. +::: + +Once you have created the extension, we can already install the extension from the local filesystem so that we can experiment with our commands as we go without having manually update it. + + + +``` +cd c8y-organizer +c8y extensions install . +``` + + + +You should see the following message if it was installed correctly. + +``` +✓ Installed extension c8y-organizer +``` + +Then you can also checkout some of the inbuilt commands (don't worry we will be modifying the commands in the following steps). + + + +```sh +# Show help +c8y organizer --help + +# Try out one of the commands from the help (using dry) +c8y organizer devices list --dry +``` + + + +:::tip +Extensions which are installed from the filesystem (e.g. local extensions) are symlinked to the extension folder. Any changes to the source extensions folder are immediately available for use. This simplifies the development process for new extensions. +::: + +### Step 2: Remove unwanted items from the extension template + +The extension template includes a lot of examples, however for this tutorial we don't need most of it. + +Run the following steps to remove the unwanted items from our new extension: + +1. Remove all the existing yaml files under `api/` as we will be creating our own files later on. + + + + ```bash + rm -f api/* + ``` + + ```powershell + Remove-Item api/* + ``` + + ```powershell + Remove-Item api/* + ``` + + + +2. Remove the `commands/` folder as we won't be including any script based commands in this tutorial. + + + + ```bash + rm -rf commands/ + ``` + + ```powershell + Remove-Item commands -Recurse + ``` + + ```powershell + Remove-Item commands -Recurse + ``` + + + +### Step 3: Add a command group + +Now we want to create a new command group, so before we do that let's open up the extension folder in an editor like VS Code (VS Code is recommended for this tutorial, but you can use any IDE you would like). + +Assuming you are still on the console and inside the root folder of the extension folder, `c8y-organizer`, you can open up the c8y extension created in the previous step using: + + + +```sh +code . +``` + + + +:::tip +Editing the yaml files is much easier in VSCode if you have the YAML extension (developer id `redhat.vscode-yaml`), as this adds tab completion support in the editor, so you don't have to keep looking up the schema yourself. +::: + +The YAML files under the `api/` folder are yaml based API specifications which tell `go-c8y-cli` how it should construct the command and what API call it should send when invoked. We will be adding a new file under this folder to contain all of the `assets` sub commands. + +So to create sub commands which are callable using `c8y organizer assets`, create the following file under the `api/` folder: + +```yaml title="file: api/assets.yaml" +# yaml-language-server: $schema=https://raw.githubusercontent.com/reubenmiller/go-c8y-cli/feat/extensions-manager/tools/schema/extensionCommands.json +group: + name: assets # <= sets the name of the command group + description: Manage assets + descriptionLong: | + More descriptive block which can even list include example of how to use different commands together. + + c8y organizer assets list | c8y c8y organizer assets update --name "My name" +``` + +:::note +The first section of the `api/assets.yaml` file controls the command group to which all commands defined in the file will be added under. The name of the file does not really matter, however it best practices to keep the file name aligned with the `group.name`. +::: + +Though a command groups without any commands does not make much sense, so let's move on to the next step where will be adding a command. + +### Step 4: Add the get command + +In this step, we'll be adding a single command called `get`. It is responsible for getting a single asset from our microservice. + +Add the following `commands` section to your `api/assets.yaml` file. + +```yaml title="file: api/assets.yaml" +group: # only included to show which level the commands section should be added to + name: assets + # ... + +commands: + - name: get + description: Get asset + method: GET + path: service/organizer/assets/{id} # <= It uses a variable "{id}" which is defined in `pathParameters` + pathParameters: + - name: id # <= This should have the same name as placeholder in the .path field! + type: string + description: Asset +``` + +:::note +The `commands` section is an array containing all of the commands. There can be any number of commands defined per group, but a good rule of thumb is to keep it under 10, as large number of commands is generally a sign that you haven't done enough thought on how to group your commands. +::: + +The table below describes what each of the properties represents. + +|Name|Description| +|----|-----------| +|`name`| Name of the command | +|`method`| Which HTTP method to use | +|`path`| HTTP request path | +|`pathParameters`| List of parameters which will be mapped to command flags, and used to substitute any placeholders in the `.path` property | + +Notice the `path` is using a variable, `{id}`. This means that it requires a parameter to be defined elsewhere so that the REST API request gets can replace the `{id}` placeholder with the asset id given by the user. Since it the `{id}` is defined in the path, it means that we need to define a parameter under the `pathParameters`. Each of these parameters will be exposed to the user via flags of the command line. + +Now let's check if the command is doing what it should. Running the command with the --dry argument is recommended until you're sure you did everything correctly. + + + + +```bash +c8y organizer assets get --id 12345 --dry +``` + + + +```bash title="Output" +What If: Sending [GET] request to [https://{host}/service/organizer/assets/12345] + +### GET /service/organizer/assets/12345 +``` + +Now let's say that our fictitious API also supports a query parameter to determine if the user would like to include extra details about the device, then we can modify the above snippet to also provide a `queryParameters` section which accepts a list of parameter (exactly like the `pathParameters`) however the parameters are mapped to query parameters instead of the path. + +Below adds the `detailed` parameter which is a boolean. The boolean type is mapped to a flag which does not accept an argument, e.g. `--detailed`. If `--detailed` is not present, then the query parameter is not added to the outgoing request. + +```yaml +# yaml-language-server: $schema=https://raw.githubusercontent.com/reubenmiller/go-c8y-cli/feat/extensions-manager/tools/schema/extensionCommands.json +commands: + - name: get + description: Get asset + method: GET + path: service/organizer/assets/{id} + pathParameters: + - name: id # <= This should have the same name as placeholder in the .path field! + type: string + description: Asset + + queryParameters: + - name: detailed + type: boolean + description: Include detailed values +``` + +Below show the new `detailed` parameter in action: + + + +```bash +c8y organizer assets get --id 12345 --detailed --dry +``` + + + +```bash title="Output" +What If: Sending [GET] request to [https://{host}/service/organizer/assets/12345?detailed=true] + +### GET /service/organizer/assets/12345?detailed=true +``` + +:::note +There are a lot of different types that you can use when building your commands (not just `string`, `boolean` etc.), so there is likely to be one that meets your need. +::: + + +### Step 5: Add tab completion + +Tab completion is a very useful feature which saves the user looking up things themselves as they can just press `` and select an option from the response. + +Some parameter types (such as `device[]` and `application[]`) include built-in tab completion and named lookups, however if you don't find any types that meet your exact need then you can use the external tab completion option. The external tab completion mechanism allows you to execute another `c8y` command, or a shell of your choosing, to provide the completion values that should be displayed to the user. + +Below shows an example of an external completion which uses the `c8y devices list` to provide the device names (with the device id being shown in the option's description for more context). + +```yaml +commands: + - name: get + description: Get asset + method: GET + path: service/organizer/assets/{id} + pathParameters: + - name: id + type: string + description: Asset + completion: # <= Provide tab completion via another c8y command + type: external + command: + - c8y + - devices + - list + - --query + - name eq '%s' + - --select=name,id +``` + +The following shows the example tab completion output which is now presented to the user. + + + +```bash +c8y organizer assets get --id +``` + + + +```bash title="Output" +linux_001 -- | id: 441672938 +linux_002 -- | id: 401669543 +``` + +:::note +The `--select ` flag used in the completion command is used to control which data . The first value is the value which will be returned to the user, and the additional columns are used to provide additional context to the user to help with the selection. +::: + +:::tip +Any command including extension commands can be used to provide tab completion options. This enable maximum flexibility when creating your extension. +::: + + +### Step 6: Add named lookups + +A named lookups are similar to the external completion functions, as they allow you to define a function which takes in the user's selection and returns the actual value that should be used in the request. For example to support named lookups in the `--id` flag, the command needs to define a function to convert the name to an id. + +In most cases you can re-use the completion command and just change the `--select` to return the id. The named lookup command can be provided by the `lookup` property of a parameter. Below shows an example which re-uses the `c8y devices list` command to find any matching. Take special note of the usage of `%s` without the command. The `%s` is substituted with the current value provided by the user. + +```yaml +commands: + - name: get + description: Get asset + method: GET + path: service/organizer/assets/{id} + pathParameters: + - name: id + type: string + description: Asset + completion: + type: external + command: + - c8y + - devices + - list + - --query + - name eq '%s' + - --select=name,id + lookup: # <= Lookup a value by name + type: external + command: + - c8y + - devices + - list + - --query + - name eq '%s*' and has(c8y_IsLinux) + - --select=id +``` + +Below shows the lookup by name in action. The `linux_001` should be replaced by the actual id of the value. + + + +```bash +c8y organizer assets get --id linux_001 --dry +``` + + + +```bash title="Output" +What If: Sending [GET] request to [https://{host}/service/organizer/assets/441672938] + +### GET /service/organizer/assets/441672938 +``` + +:::note +If the lookup command returns multiple matches, the first result will be used. +::: + + +### Step 7: Activate pipeline functionality + +By default a command is not automatically pipeline enabled, you will have to mark a specific parameter as default flag. Any parameter/flag is allowed to accept pipeline input. + +Without pipeline support trying to use the command with piped input will result in errors. Below shows an example of such errors: + + + +```sh +# This won't work +echo 12345 | c8y organizer assets get --dry +``` + + + +```bash title="Output" +2023-05-07T16:56:35.442+0200 ERROR commandError: missing required parameters. [id] +``` + +We can fix the situation by simply adding the `pipeline: true` option to the parameter. We'll pick the `id` parameter to be the default parameter where the piped input is mapped to as it will make the command the most useful. + +```yaml +commands: + - name: get + description: Get asset + method: GET + path: service/organizer/assets/{id} + pathParameters: + - name: id + type: string + description: Asset + pipeline: true # <= Activate pipeline mapping for the --id flag + pipelineAliases: # <= Optional: Additional properties to look for the --id value when json is being piped + - deviceId + - source.id + - id + completion: + type: external + command: + - c8y + - devices + - list + - --query + - name eq '%s*' and has(c8y_IsLinux) + - --select=name,type,id + lookup: + type: external + command: + - c8y + - devices + - list + - --query + - (name eq '%s*') and has(c8y_IsLinux) + - --select=id + queryParameters: + - name: detailed + type: boolean + description: Include detailed values +``` + +After the `--id` parameter has been marked as accepting pipeline, then any input data is now magically mapped to the `id` flag, take special note of the HTTP path variable. + + + +```sh +echo 12345 | c8y organizer assets get --dry +``` + + + +```bash title="Output" +What If: Sending [GET] request to [https://{host}/service/organizer/assets/12345] + +### GET /service/organizer/assets/12345 +``` + +:::note +If you forget to mark a flag with the `pipeline: true` option, then the user can still manually map pipeline input to a specific flag using the `-` value. + +```sh +# --id will get the value from the piped input +echo 12345 | c8y organizer assets get --id - --dry +``` +::: + + +### Step 8: Add remaining commands and use YAML anchors + +YAML anchors can be used to minimize the amount of copy/pasting requires when creating a spec. + +For example, we want to support external completion and named lookups on the `id` parameter, however it is used in multiple commands (e.g. `get`, `update` and `delete`). To prevent duplication we can create a custom type on the root level, give it an alias `type-asset` and then reference it later on. + +To start off, let's create a re-usable snippet to contain the configuration for the `id` parameter and called it `x-type-asset`. It will be defined on the root level of the YAML document, and we'll assign it an anchor called `type-asset`. Below shows the snippet: + + +```yaml +# Use can use yaml anchors to reduce the amount of boilerplate +x-type-asset: &type-asset + type: string + description: Device. It support a custom completion/lookup using other c8y commands + pipeline: true + completion: + type: external + command: + - c8y + - devices + - list + - --query + - "name eq '%s*'" + - --select=name + lookup: + type: external + command: + - c8y + - devices + - list + - --query + - "name eq '%s*'" + - --select=id +``` + +The `type-asset` anchor can then be reused through the YAML specification where applicable. + +Below shows the final API specification after all the commands have been added to it and the `id` parameters reference the `type-asset` anchor (using the slightly obscure but useful YAML syntax `<<: *type-asset`): + +```yaml title="file: api/assets.yaml" +# yaml-language-server: $schema=https://raw.githubusercontent.com/reubenmiller/go-c8y-cli/feat/extensions-manager/tools/schema/extensionCommands.json +--- +group: + name: assets + description: Manage assets + descriptionLong: | + More descriptive block which can even list include example of how to use different commands together. + c8y organizer devices list | c8y c8y organizer update --name "My name" + +# Use YAML anchor which can be referenced by the parameters +x-type-asset: &type-asset + type: string + description: Asset + pipeline: true + pipelineAliases: + - id + - deviceId + - source.id + completion: + type: external + command: + - c8y + - devices + - list + - --query + - name eq '%s*' and has(c8y_IsLinux) + - --select=name,type,id + lookup: + type: external + options: + idPattern: '^[0-9]+$' + command: + - c8y + - devices + - list + - --query + - (name eq '%s*') and has(c8y_IsLinux) + - --select=id + +commands: + - name: list + description: Get asset collection + method: GET + path: service/organizer + + - name: get + description: Get asset + method: GET + path: service/organizer/assets/{id} + pathParameters: + - name: id + <<: *type-asset + queryParameters: + - name: detailed + type: boolean + description: Include detailed values + + - name: update + description: Update asset + method: PUT + path: service/organizer/assets/{id} + pathParameters: + - name: id + <<: *type-asset + + - name: delete + description: Delete asset + method: DELETE + path: service/organizer/assets/{id} + pathParameters: + - name: id + <<: *type-asset + + - name: create + description: Create asset + method: POST + path: service/organizer/assets/ +``` diff --git a/docs/go-c8y-cli/docs/tutorials/extensions/index.md b/docs/go-c8y-cli/docs/tutorials/extensions/index.md new file mode 100644 index 000000000..a3c921a70 --- /dev/null +++ b/docs/go-c8y-cli/docs/tutorials/extensions/index.md @@ -0,0 +1,12 @@ +--- +category: Tutorials - Extensions +title: Extensions +--- + +import DocCardList from '@theme/DocCardList'; + +## Overview + +The following tutorial contain examples on how to create your own extensions. Checkout the [concepts page](/docs/concepts/extensions/) to find out more information on what an extension is. + + diff --git a/docs/go-c8y-cli/docusaurus.config.js b/docs/go-c8y-cli/docusaurus.config.js index 7f9833e14..577e4d18c 100644 --- a/docs/go-c8y-cli/docusaurus.config.js +++ b/docs/go-c8y-cli/docusaurus.config.js @@ -161,9 +161,9 @@ const baseUrl = `${process.env.BASE_URL || '/'}`; // anonymizeIP: true, // }, announcementBar: { - id: 'v2-major-release', + id: 'extensions', content: - '🎉 go-c8y-cli v2 is now supports linux natively (pipelines and everything)! Check out the installation instructions', + '📦 go-c8y-cli now supports extensions. Install the latest version (>=2.30.0) and check out the concepts page 🚀', }, navbar: { title: 'go-c8y-cli', diff --git a/docs/go-c8y-cli/src/components/CodeBlock.jsx b/docs/go-c8y-cli/src/components/CodeBlock.jsx index 6561e661f..f67889015 100644 --- a/docs/go-c8y-cli/src/components/CodeBlock.jsx +++ b/docs/go-c8y-cli/src/components/CodeBlock.jsx @@ -98,51 +98,31 @@ const c8yCommands = { }; +const powershellCommands = { + 'rm': 'Remove-Item', +}; + function replaceAll(string, search, replace) { return string.split(search).join(replace); } -function convertToCmdlets(code = '') { - const keys = Object.keys(c8yCommands); +function convertToCmdlets(code, commands) { + const keys = Object.keys(commands); for (let index = 0; index < keys.length; index++) { - const element = c8yCommands[keys[index]]; + const element = commands[keys[index]]; code = replaceAll(code, keys[index], element); } code = code.replace(/\\/g, '`'); return code; } -function getCmdlet(parts) { - let cmdlet = ''; - let prefix = []; - let lastIdx = 0; - for (let i = 0; i < parts.length; i++) { - if (parts[i].startsWith('-')) { - break; - } - prefix.push(parts[i]); - lastIdx++; - } - - if (parts.length > 2) { - cmdlet = c8yCommands[prefix.join(' ')] - } - - if (!cmdlet) { - return parts - } - return [cmdlet, ...parts.slice(lastIdx)] -} - function transformToPowerShell(code = '') { - let parts = convertToCmdlets(code).split(' '); - - // parts = getCmdlet(parts); + let parts = convertToCmdlets(code, c8yCommands).split(' '); if (parts.length) { for (let i = 0; i < parts.length; i++) { if (parts[i].startsWith('--')) { - parts[i] = '-' + parts[i].substr(2, 1).toUpperCase() + parts[i].substr(3) + parts[i] = '-' + parts[i].substring(2, 1).toUpperCase() + parts[i].substring(3) } else if (parts[i].startsWith('-')) { } } @@ -150,9 +130,13 @@ function transformToPowerShell(code = '') { return parts.join(' '); } +function transformCommonShellCommandsToPowershell(code = '') { + return convertToCmdlets(code, powershellCommands).split(' ').join(' '); +} export default ({ children, className = 'bash', live = false, render = false, transform = false }) => { - const { isDarkTheme } = useColorMode(); + const { colorMode } = useColorMode(); + const isDarkTheme = colorMode === "dark"; if (live) { return (
@@ -180,13 +164,13 @@ export default ({ children, className = 'bash', live = false, render = false, tr } let childrenCode = ''; if (children && typeof children.trim == 'function') { - if (`${transform}` == 'true') { - childrenCode = transformToPowerShell(children.trim()); - } else { - childrenCode = children.trim(); - if (className === 'powershell') { - childrenCode = childrenCode.replace(/\\/g, '`') + childrenCode = children.trim(); + if (className === 'powershell') { + if (`${transform}` == 'true') { + childrenCode = transformToPowerShell(children.trim()); } + childrenCode = transformCommonShellCommandsToPowershell(childrenCode); + childrenCode = childrenCode.replace(/\\/g, '`'); } } diff --git a/go.mod b/go.mod index fae5c332d..75ce4e7c1 100644 --- a/go.mod +++ b/go.mod @@ -7,28 +7,28 @@ require ( github.com/charmbracelet/glamour v0.6.0 github.com/cli/safeexec v1.0.1 github.com/cpuguy83/go-md2man/v2 v2.0.2 - github.com/fatih/color v1.14.1 - github.com/google/go-jsonnet v0.19.1 + github.com/fatih/color v1.15.0 + github.com/google/go-jsonnet v0.20.0 github.com/google/shlex v0.0.0-20191202100458-e7afc7fbc510 github.com/gorilla/websocket v1.5.0 github.com/howeyc/gopass v0.0.0-20210920133722-c8aef6fb66ef github.com/karrick/tparse/v2 v2.8.2 github.com/manifoldco/promptui v0.9.0 github.com/mattn/go-colorable v0.1.13 - github.com/mattn/go-isatty v0.0.17 + github.com/mattn/go-isatty v0.0.18 github.com/mdp/qrterminal v1.0.1 github.com/mitchellh/go-homedir v1.1.0 - github.com/muesli/termenv v0.14.0 + github.com/muesli/termenv v0.15.1 github.com/obeattie/ohmyglob v0.0.0-20150811221449-290764208a0d github.com/olekukonko/tablewriter v0.0.5 github.com/olekukonko/ts v0.0.0-20171002115256-78ecb04241c0 github.com/pkg/errors v0.9.1 github.com/pquerna/otp v1.4.0 - github.com/reubenmiller/go-c8y v0.13.0-rc.1 - github.com/santhosh-tekuri/jsonschema/v5 v5.2.0 + github.com/reubenmiller/go-c8y v0.13.0-rc.3 + github.com/santhosh-tekuri/jsonschema/v5 v5.3.0 github.com/sergi/go-diff v1.3.1 // indirect github.com/sethvargo/go-password v0.2.0 - github.com/spf13/cobra v1.6.1 + github.com/spf13/cobra v1.7.0 github.com/spf13/pflag v1.0.5 github.com/spf13/viper v1.15.0 github.com/stretchr/testify v1.8.2 @@ -37,8 +37,8 @@ require ( github.com/tidwall/sjson v1.2.5 github.com/vbauerster/mpb/v6 v6.0.4 go.uber.org/zap v1.24.0 - golang.org/x/crypto v0.7.0 - golang.org/x/term v0.6.0 + golang.org/x/crypto v0.9.0 + golang.org/x/term v0.8.0 gopkg.in/yaml.v3 v3.0.1 moul.io/http2curl/v2 v2.3.0 ) @@ -54,11 +54,12 @@ require ( github.com/VividCortex/ewma v1.2.0 // indirect github.com/acarl005/stripansi v0.0.0-20180116102854-5a71ef0e047d // indirect github.com/alecthomas/chroma v0.10.0 // indirect - github.com/aymanbagabas/go-osc52 v1.2.2 // indirect + github.com/aymanbagabas/go-osc52/v2 v2.0.1 // indirect github.com/aymerick/douceur v0.2.0 // indirect github.com/boombuler/barcode v1.0.1 // indirect github.com/chzyer/readline v1.5.1 // indirect github.com/cihub/seelog v0.0.0-20170130134532-f561c5e57575 // indirect + github.com/creack/pty v1.1.18 // indirect github.com/davecgh/go-spew v1.1.1 // indirect github.com/dlclark/regexp2 v1.8.1 // indirect github.com/fsnotify/fsnotify v1.6.0 // indirect @@ -89,9 +90,9 @@ require ( github.com/yuin/goldmark-emoji v1.0.1 // indirect go.uber.org/atomic v1.10.0 // indirect go.uber.org/multierr v1.9.0 // indirect - golang.org/x/net v0.8.0 // indirect - golang.org/x/sys v0.6.0 // indirect - golang.org/x/text v0.8.0 // indirect + golang.org/x/net v0.10.0 // indirect + golang.org/x/sys v0.8.0 // indirect + golang.org/x/text v0.9.0 // indirect gopkg.in/ini.v1 v1.67.0 // indirect gopkg.in/tomb.v2 v2.0.0-20161208151619-d5d1b5820637 // indirect gopkg.in/yaml.v2 v2.4.0 // indirect diff --git a/go.sum b/go.sum index 1f8bc3ae8..430620e22 100644 --- a/go.sum +++ b/go.sum @@ -78,9 +78,8 @@ github.com/armon/go-metrics v0.3.10/go.mod h1:4O98XIr/9W0sxpJ8UaYkvjk10Iff7SnFrb github.com/armon/go-radix v0.0.0-20180808171621-7fddfc383310/go.mod h1:ufUuZ+zHj4x4TnLV4JWEpy2hxWSpsRywHrMgIH9cCH8= github.com/armon/go-radix v1.0.0/go.mod h1:ufUuZ+zHj4x4TnLV4JWEpy2hxWSpsRywHrMgIH9cCH8= github.com/aymanbagabas/go-osc52 v1.0.3/go.mod h1:zT8H+Rk4VSabYN90pWyugflM3ZhpTZNC7cASDfUCdT4= -github.com/aymanbagabas/go-osc52 v1.2.1/go.mod h1:zT8H+Rk4VSabYN90pWyugflM3ZhpTZNC7cASDfUCdT4= -github.com/aymanbagabas/go-osc52 v1.2.2 h1:NT7wkhEhPTcKnBCdPi9djmyy9L3JOL4+3SsfJyqptCo= -github.com/aymanbagabas/go-osc52 v1.2.2/go.mod h1:zT8H+Rk4VSabYN90pWyugflM3ZhpTZNC7cASDfUCdT4= +github.com/aymanbagabas/go-osc52/v2 v2.0.1 h1:HwpRHbFMcZLEVr42D4p7XBqjyuxQH5SMiErDT4WkJ2k= +github.com/aymanbagabas/go-osc52/v2 v2.0.1/go.mod h1:uYgXzlJ7ZpABp8OJ+exZzJJhRNQ2ASbcXHWsFqH8hp8= github.com/aymerick/douceur v0.2.0 h1:Mv+mAeH1Q+n9Fr+oyamOlAkUNPWPlA8PPGR0QAaYuPk= github.com/aymerick/douceur v0.2.0/go.mod h1:wlT5vV2O3h55X9m7iVYN0TBM0NH/MmbLnd30/FjWUq4= github.com/benbjohnson/clock v1.1.0 h1:Q92kusRqC1XV2MjkWETPvjJVqKetz1OzxZB7mHJLju8= @@ -132,8 +131,9 @@ github.com/coreos/go-systemd/v22 v22.3.2/go.mod h1:Y58oyj3AT4RCenI/lSvhwexgC+NSV github.com/cpuguy83/go-md2man/v2 v2.0.2 h1:p1EgwI/C7NhT0JmVkwCD2ZBK8j4aeHQX2pMHHBfMQ6w= github.com/cpuguy83/go-md2man/v2 v2.0.2/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o= github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E= -github.com/creack/pty v1.1.17 h1:QeVUsEDNrLBW4tMgZHvxy18sKtr6VI492kBhUfhDJNI= github.com/creack/pty v1.1.17/go.mod h1:MOBLtS5ELjhRRrroQr9kyvTxUAFNvYEK993ew/Vr4O4= +github.com/creack/pty v1.1.18 h1:n56/Zwd5o6whRC5PMGretI4IdRLlmBXYNjScPaBgsbY= +github.com/creack/pty v1.1.18/go.mod h1:MOBLtS5ELjhRRrroQr9kyvTxUAFNvYEK993ew/Vr4O4= github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= @@ -154,10 +154,9 @@ github.com/envoyproxy/protoc-gen-validate v0.6.2/go.mod h1:2t7qjJNvHPx8IjnBOzl9E github.com/fatih/color v1.7.0/go.mod h1:Zm6kSWBoL9eyXnKyktHP6abPY2pDugNf5KwzbycvMj4= github.com/fatih/color v1.9.0/go.mod h1:eQcE1qtQxscV5RaZvpXrrb8Drkc3/DdQ+uUYCNjL+zU= github.com/fatih/color v1.10.0/go.mod h1:ELkj/draVOlAH/xkhN6mQ50Qd0MPOk5AAr3maGEBuJM= -github.com/fatih/color v1.12.0/go.mod h1:ELkj/draVOlAH/xkhN6mQ50Qd0MPOk5AAr3maGEBuJM= github.com/fatih/color v1.13.0/go.mod h1:kLAiJbzzSOZDVNGyDpeOxJ47H46qBXwg5ILebYFFOfk= -github.com/fatih/color v1.14.1 h1:qfhVLaG5s+nCROl1zJsZRxFeYrHLqWroPOQ8BWiNb4w= -github.com/fatih/color v1.14.1/go.mod h1:2oHN61fhTpgcxD3TSWCgKDiH1+x4OiDVVGH8WlgGZGg= +github.com/fatih/color v1.15.0 h1:kOqh6YHBtK8aywxGerMG2Eq3H6Qgoqeo13Bk2Mv/nBs= +github.com/fatih/color v1.15.0/go.mod h1:0h5ZqXfHYED7Bhv2ZJamyIOUej9KtShiJESRwBDUSsw= github.com/frankban/quicktest v1.14.3 h1:FJKSZTDHjyhriyC81FLQ0LY93eSai0ZyR/ZIkd3ZUKE= github.com/fsnotify/fsnotify v1.5.1/go.mod h1:T3375wBYaZdLLcVNkcVbzGHY7f1l/uK5T5Ai1i3InKU= github.com/fsnotify/fsnotify v1.6.0 h1:n+5WquG0fcWoWp6xPWfHdbskMCQaFnG6PfBrh1Ky4HY= @@ -228,8 +227,8 @@ github.com/google/go-cmp v0.5.6/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/ github.com/google/go-cmp v0.5.7/go.mod h1:n+brtR0CgQNWTVd5ZUFpTBC8YFBDLK/h/bpaJ8/DtOE= github.com/google/go-cmp v0.5.9 h1:O2Tfq5qg4qc4AmwVlvv0oLiVAGB7enBSJ2x2DqQFi38= github.com/google/go-jsonnet v0.18.0/go.mod h1:C3fTzyVJDslXdiTqw/bTFk7vSGyCtH3MGRbDfvEwGd0= -github.com/google/go-jsonnet v0.19.1 h1:MORxkrG0elylUqh36R4AcSPX0oZQa9hvI3lroN+kDhs= -github.com/google/go-jsonnet v0.19.1/go.mod h1:5JVT33JVCoehdTj5Z2KJq1eIdt3Nb8PCmZ+W5D8U350= +github.com/google/go-jsonnet v0.20.0 h1:WG4TTSARuV7bSm4PMB4ohjxe33IHT5WVTrJSU33uT4g= +github.com/google/go-jsonnet v0.20.0/go.mod h1:VbgWF9JX7ztlv770x/TolZNGGFfiHEVx9G6ca2eUmeA= github.com/google/go-querystring v1.1.0 h1:AnCroh3fv4ZBgVIf1Iwtovgjaw/GiKJo8M8yD/fhyJ8= github.com/google/go-querystring v1.1.0/go.mod h1:Kcdr2DB4koayq7X8pmAG4sNG59So17icRSOU623lUBU= github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= @@ -304,7 +303,6 @@ github.com/howeyc/gopass v0.0.0-20210920133722-c8aef6fb66ef/go.mod h1:lADxMC39cJ github.com/iancoleman/strcase v0.2.0/go.mod h1:iwCmte+B7n89clKwxIoIXy/HfoL7AsD47ZCWhYzw7ho= github.com/ianlancetaylor/demangle v0.0.0-20181102032728-5e5cf60278f6/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc= github.com/ianlancetaylor/demangle v0.0.0-20200824232613-28f6c0f3b639/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc= -github.com/inconshreveable/mousetrap v1.0.1/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw= github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8= github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw= github.com/jpillora/backoff v1.0.0/go.mod h1:J/6gKK9jxlEcS3zixgDgUAsiuZ7yrSoa/FX5e0EB2j4= @@ -362,8 +360,8 @@ github.com/mattn/go-isatty v0.0.11/go.mod h1:PhnuNfih5lzO57/f3n+odYbM4JtupLOxQOA github.com/mattn/go-isatty v0.0.12/go.mod h1:cbi8OIDigv2wuxKPP5vlRcQ1OAZbq2CE4Kysco4FUpU= github.com/mattn/go-isatty v0.0.14/go.mod h1:7GGIvUiUoEMVVmxf/4nioHXj79iQHKdU27kJ6hsGG94= github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM= -github.com/mattn/go-isatty v0.0.17 h1:BTarxUcIeDqL27Mc+vyvdWYSL28zpIhv3RoTdsLMPng= -github.com/mattn/go-isatty v0.0.17/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM= +github.com/mattn/go-isatty v0.0.18 h1:DOKFKCQ7FNG2L1rbrmstDN4QVRdS89Nkh85u68Uwp98= +github.com/mattn/go-isatty v0.0.18/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= github.com/mattn/go-runewidth v0.0.9/go.mod h1:H031xJmbD/WCDINGzjvQ9THkh0rPKHF+m2gUSrubnMI= github.com/mattn/go-runewidth v0.0.10/go.mod h1:RAqKPSqVFrSLVXbA8x7dzmKdmGzieGRCM46jaSJTDAk= github.com/mattn/go-runewidth v0.0.12/go.mod h1:RAqKPSqVFrSLVXbA8x7dzmKdmGzieGRCM46jaSJTDAk= @@ -398,8 +396,8 @@ github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjY github.com/muesli/reflow v0.3.0 h1:IFsN6K9NfGtjeggFP+68I4chLZV2yIKsXJFNZ+eWh6s= github.com/muesli/reflow v0.3.0/go.mod h1:pbwTDkVPibjO2kyvBQRBxTWEEGDGq0FlB1BIKtnHY/8= github.com/muesli/termenv v0.13.0/go.mod h1:sP1+uffeLaEYpyOTb8pLCUctGcGLnoFjSn4YJK5e2bc= -github.com/muesli/termenv v0.14.0 h1:8x9NFfOe8lmIWK4pgy3IfVEy47f+ppe3tUqdPZG2Uy0= -github.com/muesli/termenv v0.14.0/go.mod h1:kG/pF1E7fh949Xhe156crRUrHNyK221IuGO7Ez60Uc8= +github.com/muesli/termenv v0.15.1 h1:UzuTb/+hhlBugQz28rpzey4ZuKcZ03MeKsoG7IJZIxs= +github.com/muesli/termenv v0.15.1/go.mod h1:HeAQPTzpfs016yGtA4g00CsdYnVLJvxsS4ANqrZs2sQ= github.com/mwitkow/go-conntrack v0.0.0-20161129095857-cc309e4a2223/go.mod h1:qRWi+5nqEBWmkhHvq77mSJWrCKwh8bxhgT7d/eI7P4U= github.com/mwitkow/go-conntrack v0.0.0-20190716064945-2f068394615f/go.mod h1:qRWi+5nqEBWmkhHvq77mSJWrCKwh8bxhgT7d/eI7P4U= github.com/obeattie/ohmyglob v0.0.0-20150811221449-290764208a0d h1:SIsYJszBZOjUGo91Z3x3LufvYRZ2CwB+F4EKK5b53iw= @@ -449,8 +447,8 @@ github.com/prometheus/procfs v0.0.8/go.mod h1:7Qr8sr6344vo1JqZ6HhLceV9o3AJ1Ff+Gx github.com/prometheus/procfs v0.1.3/go.mod h1:lV6e/gmhEcM9IjHGsFOCxxuZ+z1YqCvr4OA4YeYWdaU= github.com/prometheus/procfs v0.6.0/go.mod h1:cz+aTbrPOrUb4q7XlbU9ygM+/jj0fzG6c1xBZuNvfVA= github.com/prometheus/procfs v0.7.3/go.mod h1:cz+aTbrPOrUb4q7XlbU9ygM+/jj0fzG6c1xBZuNvfVA= -github.com/reubenmiller/go-c8y v0.13.0-rc.1 h1:0mNtzUbqd9LBPDAPunc3EegdOd0HrQTA21tEiRaS2NY= -github.com/reubenmiller/go-c8y v0.13.0-rc.1/go.mod h1:t+biKGzNJKWx0A8inMl6xkmVWKuRyTe36+Mq/kFLBkM= +github.com/reubenmiller/go-c8y v0.13.0-rc.3 h1:d63nfVZAQ5RojwvaFR+4T0//+pYDVegLPh+c3wbRqew= +github.com/reubenmiller/go-c8y v0.13.0-rc.3/go.mod h1:t+biKGzNJKWx0A8inMl6xkmVWKuRyTe36+Mq/kFLBkM= github.com/reubenmiller/gojsonq/v2 v2.0.0-20221119213524-0fd921ac20a3 h1:v8Q77ObTxkm0Wj9iAjcc0VMLxqEzKIdAnaTNPzSiw8Q= github.com/reubenmiller/gojsonq/v2 v2.0.0-20221119213524-0fd921ac20a3/go.mod h1:QidmUT4ebNVwyjKXAQgx9VFHxpOxBKWs32EEXaXnEfE= github.com/rivo/uniseg v0.1.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc= @@ -464,8 +462,8 @@ github.com/russross/blackfriday/v2 v2.1.0 h1:JIOH55/0cWyOuilr9/qlrm0BSXldqnqwMsf github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= github.com/ryanuber/columnize v0.0.0-20160712163229-9b3edd62028f/go.mod h1:sm1tb6uqfes/u+d4ooFouqFdy9/2g9QGwK3SQygK0Ts= github.com/sagikazarmark/crypt v0.4.0/go.mod h1:ALv2SRj7GxYV4HO9elxH9nS6M9gW+xDNxqmyJ6RfDFM= -github.com/santhosh-tekuri/jsonschema/v5 v5.2.0 h1:WCcC4vZDS1tYNxjWlwRJZQy28r8CMoggKnxNzxsVDMQ= -github.com/santhosh-tekuri/jsonschema/v5 v5.2.0/go.mod h1:FKdcjfQW6rpZSnxxUvEA5H/cDPdvJ/SZJQLWWXWGrZ0= +github.com/santhosh-tekuri/jsonschema/v5 v5.3.0 h1:uIkTLo0AGRc8l7h5l9r+GcYi9qfVPt6lD4/bhmzfiKo= +github.com/santhosh-tekuri/jsonschema/v5 v5.3.0/go.mod h1:FKdcjfQW6rpZSnxxUvEA5H/cDPdvJ/SZJQLWWXWGrZ0= github.com/scylladb/termtables v0.0.0-20191203121021-c4c0b6d42ff4/go.mod h1:C1a7PQSMz9NShzorzCiG2fk9+xuCgLkPeCvMHYR2OWg= github.com/sean-/seed v0.0.0-20170313163322-e2103e2c3529/go.mod h1:DxrIzT+xaE7yg65j358z/aeFdxmN0P9QXhEzd20vsDc= github.com/sergi/go-diff v1.0.0/go.mod h1:0CfEIISq7TuYL3j771MWULgwwjU+GofnZX9QAmXWZgo= @@ -486,8 +484,8 @@ github.com/spf13/afero v1.9.4/go.mod h1:iUV7ddyEEZPO5gA3zD4fJt6iStLlL+Lg4m2cihcD github.com/spf13/cast v1.4.1/go.mod h1:Qx5cxh0v+4UWYiBimWS+eyWzqEqokIECu5etghLkUJE= github.com/spf13/cast v1.5.0 h1:rj3WzYc11XZaIZMPKmwP96zkFEnnAmV8s6XbB2aY32w= github.com/spf13/cast v1.5.0/go.mod h1:SpXXQ5YoyJw6s3/6cMTQuxvgRl3PCJiyaX9p6b155UU= -github.com/spf13/cobra v1.6.1 h1:o94oiPyS4KD1mPy2fmcYYHHfCxLqYjJOhGsCHFZtEzA= -github.com/spf13/cobra v1.6.1/go.mod h1:IOw/AERYS7UzyrGinqmz6HLUo219MORXGxhbaJUqzrY= +github.com/spf13/cobra v1.7.0 h1:hyqWnYt1ZQShIddO5kBpj3vu05/++x6tJ6dg8EC572I= +github.com/spf13/cobra v1.7.0/go.mod h1:uLxZILRyS/50WlhOIKD7W6V5bgeIt+4sICxh6uRMrb0= github.com/spf13/jwalterweatherman v1.1.0 h1:ue6voC5bR5F8YxI5S67j9i582FU4Qvo2bmqnqMYADFk= github.com/spf13/jwalterweatherman v1.1.0/go.mod h1:aNWZUN0dPAAO/Ljvb5BEdw96iTZ0EXowPYD95IqWIGo= github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA= @@ -578,8 +576,8 @@ golang.org/x/crypto v0.0.0-20210421170649-83a5a9bb288b/go.mod h1:T9bdIzuCu7OtxOm golang.org/x/crypto v0.0.0-20210817164053-32db794688a5/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= golang.org/x/crypto v0.0.0-20211108221036-ceb1ce70b4fa/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= golang.org/x/crypto v0.0.0-20220112180741-5e0467b6c7ce/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4= -golang.org/x/crypto v0.7.0 h1:AvwMYaRytfdeVt3u6mLaxYtErKYjxA2OXjJ1HHq6t3A= -golang.org/x/crypto v0.7.0/go.mod h1:pYwdfH91IfpZVANVyUOhSIPZaFoJGxTFbZhFTx+dXZU= +golang.org/x/crypto v0.9.0 h1:LF6fAI+IutBocDJ2OT0Q1g8plpYljMZ4+lty+dsqw3g= +golang.org/x/crypto v0.9.0/go.mod h1:yrmDGqONDYtNj3tH8X9dzUun2m2lzPa9ngI6/RUPGR0= golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= golang.org/x/exp v0.0.0-20190306152737-a1d7652674e8/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= golang.org/x/exp v0.0.0-20190510132918-efd6b22b2522/go.mod h1:ZjyILWgesfNpC6sMxTJOJm9Kp84zZh5NQWvqDGG3Qr8= @@ -663,8 +661,8 @@ golang.org/x/net v0.0.0-20211015210444-4f30a5c0130f/go.mod h1:9nx3DQGgdP8bBQD5qx golang.org/x/net v0.0.0-20211112202133-69e39bad7dc2/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= golang.org/x/net v0.0.0-20220121210141-e204ce36a2ba/go.mod h1:CfG3xpIq0wQ8r1q4Su4UZFWDARRcnwPjda9FqA0JpMk= golang.org/x/net v0.0.0-20221002022538-bcab6841153b/go.mod h1:YDH+HFinaLZZlnHAfSS6ZXJJ9M9t4Dl22yv3iI2vPwk= -golang.org/x/net v0.8.0 h1:Zrh2ngAOFYneWTAIAPethzeaQLuHwhuBkuV6ZiRnUaQ= -golang.org/x/net v0.8.0/go.mod h1:QVkue5JL9kW//ek3r6jTKnTFis1tRmNAW2P1shuFdJc= +golang.org/x/net v0.10.0 h1:X2//UzNDwYmtCLn7To6G58Wr6f5ahEAQgKNzv9Y951M= +golang.org/x/net v0.10.0/go.mod h1:0qNGK6F8kojg2nk9dLZ2mShWaEBan6FAoqfSigmmuDg= golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= golang.org/x/oauth2 v0.0.0-20190226205417-e64efc72b421/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= golang.org/x/oauth2 v0.0.0-20190604053449-0f29369cfe45/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= @@ -775,14 +773,14 @@ golang.org/x/sys v0.0.0-20220422013727-9388b58f7150/go.mod h1:oPkhp1MJrh7nUepCBc golang.org/x/sys v0.0.0-20220728004956-3c1f35247d10/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220908164124-27713097b956/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.1.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.6.0 h1:MVltZSvRTcU2ljQOhs94SXPftV6DCNnZViHeQps87pQ= golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.8.0 h1:EBmGv8NaZBZTWvrbjNoL6HVt+IVy3QDQpJs7VRIw3tU= +golang.org/x/sys v0.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= golang.org/x/term v0.0.0-20210503060354-a79de5458b56/go.mod h1:tfny5GFUkzUvx4ps4ajbZsCe5lw1metzhBm9T3x7oIY= golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= -golang.org/x/term v0.6.0 h1:clScbb1cHjoCkyRbWwBEUZ5H/tIFu5TAXIqaZD0Gcjw= -golang.org/x/term v0.6.0/go.mod h1:m6U89DPEgQRMq3DNkDClhWw02AUbt2daBVO4cn4Hv9U= +golang.org/x/term v0.8.0 h1:n5xxQn2i3PC0yLAbjTpNT85q/Kgzcr2gIoX9OrJUols= +golang.org/x/term v0.8.0/go.mod h1:xPskH00ivmX89bAKVGSKKtLOWNx2+17Eiy94tnKShWo= golang.org/x/text v0.0.0-20170915032832-14c0d48ead0c/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.1-0.20180807135948-17ff2d5776d2/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= @@ -792,8 +790,8 @@ golang.org/x/text v0.3.4/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.3.5/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= -golang.org/x/text v0.8.0 h1:57P1ETyNKtuIjB4SRd15iJxuhj8Gc416Y78H3qgMh68= -golang.org/x/text v0.8.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8= +golang.org/x/text v0.9.0 h1:2sjJmO8cDvYveuX97RDLsxlyUxLl+GHoLxBiRdHllBE= +golang.org/x/text v0.9.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8= golang.org/x/time v0.0.0-20181108054448-85acf8d2951c/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= golang.org/x/time v0.0.0-20190308202827-9d24e82272b4/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= golang.org/x/time v0.0.0-20191024005414-555d28b269f0/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= diff --git a/internal/ghinstance/host.go b/internal/ghinstance/host.go new file mode 100644 index 000000000..30dafb796 --- /dev/null +++ b/internal/ghinstance/host.go @@ -0,0 +1,94 @@ +package ghinstance + +import ( + "errors" + "fmt" + "strings" +) + +const defaultHostname = "github.com" + +// localhost is the domain name of a local GitHub instance +const localhost = "github.localhost" + +// Default returns the host name of the default GitHub instance +func Default() string { + return defaultHostname +} + +// IsEnterprise reports whether a non-normalized host name looks like a GHE instance +func IsEnterprise(h string) bool { + normalizedHostName := NormalizeHostname(h) + return normalizedHostName != defaultHostname && normalizedHostName != localhost +} + +// NormalizeHostname returns the canonical host name of a GitHub instance +func NormalizeHostname(h string) string { + hostname := strings.ToLower(h) + if strings.HasSuffix(hostname, "."+defaultHostname) { + return defaultHostname + } + + if strings.HasSuffix(hostname, "."+localhost) { + return localhost + } + + return hostname +} + +func HostnameValidator(hostname string) error { + if len(strings.TrimSpace(hostname)) < 1 { + return errors.New("a value is required") + } + if strings.ContainsRune(hostname, '/') || strings.ContainsRune(hostname, ':') { + return errors.New("invalid hostname") + } + return nil +} + +func GraphQLEndpoint(hostname string) string { + if IsEnterprise(hostname) { + return fmt.Sprintf("https://%s/api/graphql", hostname) + } + if strings.EqualFold(hostname, localhost) { + return fmt.Sprintf("http://api.%s/graphql", hostname) + } + return fmt.Sprintf("https://api.%s/graphql", hostname) +} + +func RESTPrefix(hostname string) string { + if IsEnterprise(hostname) { + return fmt.Sprintf("https://%s/api/v3/", hostname) + } + if strings.EqualFold(hostname, localhost) { + return fmt.Sprintf("http://api.%s/", hostname) + } + return fmt.Sprintf("https://api.%s/", hostname) +} + +func GistPrefix(hostname string) string { + prefix := "https://" + + if strings.EqualFold(hostname, localhost) { + prefix = "http://" + } + + return prefix + GistHost(hostname) +} + +func GistHost(hostname string) string { + if IsEnterprise(hostname) { + return fmt.Sprintf("%s/gist/", hostname) + } + if strings.EqualFold(hostname, localhost) { + return fmt.Sprintf("%s/gist/", hostname) + } + return fmt.Sprintf("gist.%s/", hostname) +} + +func HostPrefix(hostname string) string { + if strings.EqualFold(hostname, localhost) { + return fmt.Sprintf("http://%s/", hostname) + } + return fmt.Sprintf("https://%s/", hostname) +} diff --git a/internal/ghinstance/host_test.go b/internal/ghinstance/host_test.go new file mode 100644 index 000000000..5feecbe63 --- /dev/null +++ b/internal/ghinstance/host_test.go @@ -0,0 +1,186 @@ +package ghinstance + +import ( + "testing" + + "github.com/stretchr/testify/assert" +) + +func TestIsEnterprise(t *testing.T) { + tests := []struct { + host string + want bool + }{ + { + host: "github.com", + want: false, + }, + { + host: "api.github.com", + want: false, + }, + { + host: "github.localhost", + want: false, + }, + { + host: "api.github.localhost", + want: false, + }, + { + host: "ghe.io", + want: true, + }, + { + host: "example.com", + want: true, + }, + } + for _, tt := range tests { + t.Run(tt.host, func(t *testing.T) { + if got := IsEnterprise(tt.host); got != tt.want { + t.Errorf("IsEnterprise() = %v, want %v", got, tt.want) + } + }) + } +} + +func TestNormalizeHostname(t *testing.T) { + tests := []struct { + host string + want string + }{ + { + host: "GitHub.com", + want: "github.com", + }, + { + host: "api.github.com", + want: "github.com", + }, + { + host: "ssh.github.com", + want: "github.com", + }, + { + host: "upload.github.com", + want: "github.com", + }, + { + host: "GitHub.localhost", + want: "github.localhost", + }, + { + host: "api.github.localhost", + want: "github.localhost", + }, + { + host: "GHE.IO", + want: "ghe.io", + }, + { + host: "git.my.org", + want: "git.my.org", + }, + } + for _, tt := range tests { + t.Run(tt.host, func(t *testing.T) { + if got := NormalizeHostname(tt.host); got != tt.want { + t.Errorf("NormalizeHostname() = %v, want %v", got, tt.want) + } + }) + } +} + +func TestHostnameValidator(t *testing.T) { + tests := []struct { + name string + input string + wantsErr bool + }{ + { + name: "valid hostname", + input: "internal.instance", + wantsErr: false, + }, + { + name: "hostname with slashes", + input: "//internal.instance", + wantsErr: true, + }, + { + name: "empty hostname", + input: " ", + wantsErr: true, + }, + { + name: "hostname with colon", + input: "internal.instance:2205", + wantsErr: true, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + err := HostnameValidator(tt.input) + if tt.wantsErr { + assert.Error(t, err) + return + } + assert.NoError(t, err) + }) + } +} +func TestGraphQLEndpoint(t *testing.T) { + tests := []struct { + host string + want string + }{ + { + host: "github.com", + want: "https://api.github.com/graphql", + }, + { + host: "github.localhost", + want: "http://api.github.localhost/graphql", + }, + { + host: "ghe.io", + want: "https://ghe.io/api/graphql", + }, + } + for _, tt := range tests { + t.Run(tt.host, func(t *testing.T) { + if got := GraphQLEndpoint(tt.host); got != tt.want { + t.Errorf("GraphQLEndpoint() = %v, want %v", got, tt.want) + } + }) + } +} + +func TestRESTPrefix(t *testing.T) { + tests := []struct { + host string + want string + }{ + { + host: "github.com", + want: "https://api.github.com/", + }, + { + host: "github.localhost", + want: "http://api.github.localhost/", + }, + { + host: "ghe.io", + want: "https://ghe.io/api/v3/", + }, + } + for _, tt := range tests { + t.Run(tt.host, func(t *testing.T) { + if got := RESTPrefix(tt.host); got != tt.want { + t.Errorf("RESTPrefix() = %v, want %v", got, tt.want) + } + }) + } +} diff --git a/internal/ghrepo/repo.go b/internal/ghrepo/repo.go new file mode 100644 index 000000000..0af7cc507 --- /dev/null +++ b/internal/ghrepo/repo.go @@ -0,0 +1,176 @@ +package ghrepo + +import ( + "fmt" + "net/url" + "strings" + + "github.com/reubenmiller/go-c8y-cli/v2/internal/ghinstance" +) + +// Interface describes an object that represents a GitHub repository +type Interface interface { + RepoName() string + RepoOwner() string + RepoHost() string + RepoURL() string +} + +func NewRepoFromHost(u string, defaultHost string) (*Respository, error) { + repo := Respository{ + host: defaultHost, + } + + parts := strings.Split(u, "/") + repoURL, err := url.Parse(u) + + if err == nil { + if repoURL.Host != "" { + repo.host = repoURL.Host + } + if repoURL.Scheme != "" { + repo.rawURL = u + } + parts = strings.Split(repoURL.Path, "/") + } + + if len(parts) >= 2 { + repo.owner = strings.Join(parts[0:len(parts)-1], "/") + repo.name = parts[len(parts)-1] + } + return &repo, nil +} + +type Respository struct { + name string + owner string + host string + rawURL string +} + +func (r *Respository) Name() string { + return r.name +} +func (r *Respository) Owner() string { + return r.owner +} +func (r *Respository) Host() string { + return r.host +} + +func (r *Respository) URL() string { + return r.rawURL +} + +// New instantiates a GitHub repository from owner and name arguments +func New(owner, repo string) Interface { + return NewWithHost(owner, repo, ghinstance.Default(), "") +} + +// NewWithHost is like New with an explicit host name +func NewWithHost(owner, repo, hostname string, fullURL string) Interface { + return &ghRepo{ + owner: owner, + name: repo, + hostname: normalizeHostname(hostname), + fullURL: fullURL, + } +} + +// FullName serializes a GitHub repository into an "OWNER/REPO" string +func FullName(r Interface) string { + return fmt.Sprintf("%s/%s", r.RepoOwner(), r.RepoName()) +} + +func defaultHost() string { + return "github.com" +} + +// FromFullName extracts the GitHub repository information from the following +// formats: "OWNER/REPO", "HOST/OWNER/REPO", and a full URL. +func FromFullName(nwo string) (Interface, error) { + return FromFullNameWithHost(nwo, defaultHost()) +} + +// FromFullNameWithHost is like FromFullName that defaults to a specific host for values that don't +// explicitly include a hostname. +func FromFullNameWithHost(nwo, fallbackHost string) (Interface, error) { + // repo, err := repository.ParseWithHost(nwo, fallbackHost) + repo, err := NewRepoFromHost(nwo, fallbackHost) + if err != nil { + return nil, err + } + return NewWithHost(repo.Owner(), repo.Name(), repo.Host(), repo.URL()), nil +} + +// FromURL extracts the GitHub repository information from a git remote URL +func FromURL(u *url.URL) (Interface, error) { + if u.Hostname() == "" { + return nil, fmt.Errorf("no hostname detected") + } + + parts := strings.SplitN(strings.Trim(u.Path, "/"), "/", 3) + + // TODO: Handle other repo sources, e.g. Azure DevOps + if len(parts) != 2 { + return nil, fmt.Errorf("invalid path: %s", u.Path) + } + + return NewWithHost(parts[0], strings.TrimSuffix(parts[1], ".git"), u.Hostname(), u.String()), nil +} + +func normalizeHostname(h string) string { + return strings.ToLower(strings.TrimPrefix(h, "www.")) +} + +// IsSame compares two GitHub repositories +func IsSame(a, b Interface) bool { + return strings.EqualFold(a.RepoOwner(), b.RepoOwner()) && + strings.EqualFold(a.RepoName(), b.RepoName()) && + normalizeHostname(a.RepoHost()) == normalizeHostname(b.RepoHost()) +} + +func GenerateRepoURL(repo Interface, p string, args ...interface{}) string { + baseURL := fmt.Sprintf("%s%s/%s", ghinstance.HostPrefix(repo.RepoHost()), repo.RepoOwner(), repo.RepoName()) + if p != "" { + if path := fmt.Sprintf(p, args...); path != "" { + return baseURL + "/" + path + } + } + return baseURL +} + +// TODO there is a parallel implementation for non-isolated commands +func FormatRemoteURL(repo Interface, protocol string) string { + if repo.RepoURL() != "" { + return repo.RepoURL() + } + if protocol == "ssh" { + return fmt.Sprintf("git@%s:%s/%s.git", repo.RepoHost(), repo.RepoOwner(), repo.RepoName()) + } + + return fmt.Sprintf("%s%s/%s.git", ghinstance.HostPrefix(repo.RepoHost()), repo.RepoOwner(), repo.RepoName()) +} + +type ghRepo struct { + owner string + name string + hostname string + fullURL string +} + +func (r ghRepo) RepoOwner() string { + return r.owner +} + +func (r ghRepo) RepoName() string { + return r.name +} + +func (r ghRepo) RepoHost() string { + return r.hostname +} + +func (r ghRepo) RepoURL() string { + return r.fullURL +} diff --git a/internal/ghrepo/repo_test.go b/internal/ghrepo/repo_test.go new file mode 100644 index 000000000..1530f8f24 --- /dev/null +++ b/internal/ghrepo/repo_test.go @@ -0,0 +1,222 @@ +package ghrepo + +import ( + "errors" + "fmt" + "net/url" + "testing" +) + +func Test_repoFromURL(t *testing.T) { + tests := []struct { + name string + input string + result string + host string + err error + }{ + { + name: "github.com URL", + input: "https://github.com/monalisa/octo-cat.git", + result: "monalisa/octo-cat", + host: "github.com", + err: nil, + }, + { + name: "github.com URL with trailing slash", + input: "https://github.com/monalisa/octo-cat/", + result: "monalisa/octo-cat", + host: "github.com", + err: nil, + }, + { + name: "www.github.com URL", + input: "http://www.GITHUB.com/monalisa/octo-cat.git", + result: "monalisa/octo-cat", + host: "github.com", + err: nil, + }, + { + name: "too many path components", + input: "https://github.com/monalisa/octo-cat/pulls", + result: "", + host: "", + err: errors.New("invalid path: /monalisa/octo-cat/pulls"), + }, + { + name: "non-GitHub hostname", + input: "https://example.com/one/two", + result: "one/two", + host: "example.com", + err: nil, + }, + { + name: "filesystem path", + input: "/path/to/file", + result: "", + host: "", + err: errors.New("no hostname detected"), + }, + { + name: "filesystem path with scheme", + input: "file:///path/to/file", + result: "", + host: "", + err: errors.New("no hostname detected"), + }, + { + name: "github.com SSH URL", + input: "ssh://github.com/monalisa/octo-cat.git", + result: "monalisa/octo-cat", + host: "github.com", + err: nil, + }, + { + name: "github.com HTTPS+SSH URL", + input: "https+ssh://github.com/monalisa/octo-cat.git", + result: "monalisa/octo-cat", + host: "github.com", + err: nil, + }, + { + name: "github.com git URL", + input: "git://github.com/monalisa/octo-cat.git", + result: "monalisa/octo-cat", + host: "github.com", + err: nil, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + u, err := url.Parse(tt.input) + if err != nil { + t.Fatalf("got error %q", err) + } + + repo, err := FromURL(u) + if err != nil { + if tt.err == nil { + t.Fatalf("got error %q", err) + } else if tt.err.Error() == err.Error() { + return + } + t.Fatalf("got error %q", err) + } + + got := fmt.Sprintf("%s/%s", repo.RepoOwner(), repo.RepoName()) + if tt.result != got { + t.Errorf("expected %q, got %q", tt.result, got) + } + if tt.host != repo.RepoHost() { + t.Errorf("expected %q, got %q", tt.host, repo.RepoHost()) + } + }) + } +} + +func TestFromFullName(t *testing.T) { + tests := []struct { + name string + input string + hostOverride string + wantOwner string + wantName string + wantHost string + wantErr error + }{ + { + name: "OWNER/REPO combo", + input: "OWNER/REPO", + wantHost: "github.com", + wantOwner: "OWNER", + wantName: "REPO", + wantErr: nil, + }, + { + name: "too few elements", + input: "OWNER", + wantErr: errors.New(`expected the "[HOST/]OWNER/REPO" format, got "OWNER"`), + }, + { + name: "too many elements", + input: "a/b/c/d", + wantErr: errors.New(`expected the "[HOST/]OWNER/REPO" format, got "a/b/c/d"`), + }, + { + name: "blank value", + input: "a/", + wantErr: errors.New(`expected the "[HOST/]OWNER/REPO" format, got "a/"`), + }, + { + name: "with hostname", + input: "example.org/OWNER/REPO", + wantHost: "example.org", + wantOwner: "OWNER", + wantName: "REPO", + wantErr: nil, + }, + { + name: "full URL", + input: "https://example.org/OWNER/REPO.git", + wantHost: "example.org", + wantOwner: "OWNER", + wantName: "REPO", + wantErr: nil, + }, + { + name: "SSH URL", + input: "git@example.org:OWNER/REPO.git", + wantHost: "example.org", + wantOwner: "OWNER", + wantName: "REPO", + wantErr: nil, + }, + { + name: "OWNER/REPO with default host override", + input: "OWNER/REPO", + hostOverride: "override.com", + wantHost: "override.com", + wantOwner: "OWNER", + wantName: "REPO", + wantErr: nil, + }, + { + name: "HOST/OWNER/REPO with default host override", + input: "example.com/OWNER/REPO", + hostOverride: "override.com", + wantHost: "example.com", + wantOwner: "OWNER", + wantName: "REPO", + wantErr: nil, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + if tt.hostOverride != "" { + t.Setenv("GH_HOST", tt.hostOverride) + } + r, err := FromFullName(tt.input) + if tt.wantErr != nil { + if err == nil { + t.Fatalf("no error in result, expected %v", tt.wantErr) + } else if err.Error() != tt.wantErr.Error() { + t.Fatalf("expected error %q, got %q", tt.wantErr.Error(), err.Error()) + } + return + } + if err != nil { + t.Fatalf("got error %v", err) + } + if r.RepoHost() != tt.wantHost { + t.Errorf("expected host %q, got %q", tt.wantHost, r.RepoHost()) + } + if r.RepoOwner() != tt.wantOwner { + t.Errorf("expected owner %q, got %q", tt.wantOwner, r.RepoOwner()) + } + if r.RepoName() != tt.wantName { + t.Errorf("expected name %q, got %q", tt.wantName, r.RepoName()) + } + }) + } +} diff --git a/internal/integration/models/api_spec.go b/internal/integration/models/api_spec.go index 73c693e11..735f0dbd8 100644 --- a/internal/integration/models/api_spec.go +++ b/internal/integration/models/api_spec.go @@ -3,11 +3,12 @@ package models import "strings" type Specification struct { - Information SpecificationInformation `yaml:"information"` - Endpoints []EndPoint `yaml:"endpoints"` + Version string `yaml:"version"` + Group Group `yaml:"group"` + Commands []Command `yaml:"commands"` } -type SpecificationInformation struct { +type Group struct { Name string `yaml:"name"` Description string `yaml:"description"` DescriptionLong string `yaml:"descriptionLong"` @@ -15,50 +16,147 @@ type SpecificationInformation struct { Skip bool `yaml:"skip"` } -type EndPoint struct { - Name string `yaml:"name"` - Method string `yaml:"method"` - Accept string `yaml:"accept,omitempty"` - CollectionType string `yaml:"collectionType,omitempty"` - CollectionProperty string `yaml:"collectionProperty,omitempty"` - Path string `yaml:"path"` - Examples Examples `yaml:"examples"` - Alias Aliases `yaml:"alias"` - Skip *bool `yaml:"skip,omitempty"` - QueryParameters []Parameter `yaml:"queryParameters,omitempty"` - PathParameters []Parameter `yaml:"pathParameters,omitempty"` - HeaderParameters []Parameter `yaml:"headerParameters,omitempty"` - Body []Parameter `yaml:"body,omitempty"` - BodyContent *BodyContent `yaml:"bodyContent,omitempty"` -} - -func (p *EndPoint) GetAllParameters() []Parameter { +type BodyTemplate struct { + Type string `yaml:"type"` + ApplyLast bool `yaml:"applyLast"` + Template string `yaml:"template"` +} + +type CommandPreset struct { + Type string `yaml:"type"` + Options map[string]string `yaml:"options,omitempty"` + Extensions []Parameter `yaml:"extensions,omitempty"` +} + +func (cp *CommandPreset) GetOption(k string, defaultValue ...string) string { + if v, ok := cp.Options[k]; ok { + return v + } + if len(defaultValue) > 0 { + return defaultValue[0] + } + return "" +} + +type Command struct { + Name string `yaml:"name"` + Description string `yaml:"description"` + DescriptionLong string `yaml:"descriptionLong"` + Preset CommandPreset `yaml:"preset"` + Deprecated string `yaml:"deprecated"` + DeprecatedAt string `yaml:"deprecatedAt"` + Method string `yaml:"method"` + SemanticMethod string `yaml:"semanticMethod"` + Accept string `yaml:"accept,omitempty"` + ContentType string `yaml:"contentType,omitempty"` + CollectionType string `yaml:"collectionType,omitempty"` + CollectionProperty string `yaml:"collectionProperty,omitempty"` + Path string `yaml:"path"` + Examples Examples `yaml:"examples"` + ExampleList []Example `yaml:"exampleList"` + Alias Aliases `yaml:"alias"` + Hidden *bool `yaml:"hidden,omitempty"` + Skip *bool `yaml:"skip,omitempty"` + QueryParameters []Parameter `yaml:"queryParameters,omitempty"` + PathParameters []Parameter `yaml:"pathParameters,omitempty"` + HeaderParameters []Parameter `yaml:"headerParameters,omitempty"` + Body []Parameter `yaml:"body,omitempty"` + BodyContent *BodyContent `yaml:"bodyContent,omitempty"` + BodyTemplates []BodyTemplate `yaml:"bodyTemplates,omitempty"` + BodyRequiredKeys []string `yaml:"bodyRequiredKeys,omitempty"` +} + +func (c *Command) HasPreset() bool { + return c.Preset.Type != "" +} + +func (c *Command) SupportsProcessingMode() bool { + return c.Method == "DELETE" || c.Method == "PUT" || c.Method == "POST" +} + +func (c *Command) IsHidden() bool { + return c.Hidden != nil && *c.Hidden +} + +func (c *Command) ShouldIgnore() bool { + return c.Skip != nil && *c.Skip +} + +func (c *Command) GetDescriptionLong() string { + var sb strings.Builder + + if c.Description != "" { + sb.WriteString(c.Description) + } + if c.DescriptionLong != "" { + sb.WriteString("\n\n") + sb.WriteString(c.DescriptionLong) + } + return sb.String() + +} + +func (c *Command) IsDeprecated() bool { + return c.Deprecated != "" +} + +func (c *Command) GetExamples() string { + var sb strings.Builder + for _, ex := range c.ExampleList { + sb.WriteString(" $ " + strings.TrimSpace(ex.Command) + "\n") + sb.WriteString(" " + strings.TrimSpace(ex.Description) + "\n\n") + } + return strings.TrimRight(sb.String(), "\n") +} + +func (c *Command) GetMethod() string { + if c.SemanticMethod != "" { + return c.SemanticMethod + } + return c.Method +} + +func (c *Command) GetAllParameters() []Parameter { parameters := make([]Parameter, 0) - if len(p.QueryParameters) > 0 { - for _, param := range p.QueryParameters { + if len(c.QueryParameters) > 0 { + for i, param := range c.QueryParameters { if len(param.Children) > 0 { parameters = append(parameters, param.Children...) } else { + c.QueryParameters[i].TargetType = ParamQueryParameter + param.TargetType = ParamQueryParameter parameters = append(parameters, param) } } } - if len(p.PathParameters) > 0 { - parameters = append(parameters, p.PathParameters...) + if len(c.PathParameters) > 0 { + for i, p := range c.PathParameters { + c.PathParameters[i].TargetType = ParamPath + p.TargetType = ParamPath + } + parameters = append(parameters, c.PathParameters...) } - if len(p.HeaderParameters) > 0 { - parameters = append(parameters, p.HeaderParameters...) + if len(c.HeaderParameters) > 0 { + for i, p := range c.HeaderParameters { + c.HeaderParameters[i].TargetType = ParamHeader + p.TargetType = ParamHeader + } + parameters = append(parameters, c.HeaderParameters...) } - if len(p.Body) > 0 { - parameters = append(parameters, p.Body...) + if len(c.Body) > 0 { + for i, p := range c.Body { + c.Body[i].TargetType = ParamBody + p.TargetType = ParamBody + } + parameters = append(parameters, c.Body...) } return parameters } -func (p *EndPoint) GetQueryParameters() []Parameter { +func (c *Command) GetQueryParameters() []Parameter { parameters := make([]Parameter, 0) - if len(p.QueryParameters) > 0 { - for _, param := range p.QueryParameters { + if len(c.QueryParameters) > 0 { + for _, param := range c.QueryParameters { if len(param.Children) > 0 { parameters = append(parameters, param.Children...) } else { @@ -69,6 +167,26 @@ func (p *EndPoint) GetQueryParameters() []Parameter { return parameters } +func (c *Command) IsCollection() bool { + return strings.EqualFold(c.Method, "GET") && + (c.CollectionProperty != "" || strings.Contains(strings.ToLower(c.Accept), "collection")) +} + +func (c *Command) SupportsTemplates() bool { + return strings.EqualFold(c.Method, "PUT") || strings.EqualFold(c.Method, "POST") +} + +func (c *Command) IsBodyFormData() bool { + return c.BodyContent != nil && c.BodyContent.Type == "formdata" +} + +func (c *Command) GetBodyContentType() string { + if c.BodyContent == nil { + return "" + } + return c.BodyContent.Type +} + type Aliases struct { Go string `yaml:"go"` PowerShell string `yaml:"powershell"` @@ -95,10 +213,14 @@ type BodyContent struct { type Parameter struct { Name string `yaml:"name,omitempty"` + ShortName string `yaml:"shortname,omitempty"` Type string `yaml:"type,omitempty"` Value string `yaml:"value,omitempty"` + Completion Completion `yaml:"completion,omitempty"` + NamedLookup NamedLookup `yaml:"lookup,omitempty"` Format string `yaml:"format,omitempty"` Property string `yaml:"property,omitempty"` + Hidden *bool `yaml:"hidden,omitempty"` Pipeline *bool `yaml:"pipeline,omitempty"` PipelineAliases []string `yaml:"pipelineAliases,omitempty"` Required *bool `yaml:"required,omitempty"` @@ -108,6 +230,57 @@ type Parameter struct { ValidationSet []string `yaml:"validationSet,omitempty"` Skip *bool `yaml:"skip,omitempty"` Children []Parameter `yaml:"children,omitempty"` + DependsOn []string `yaml:"dependsOn,omitempty"` + + TargetType TargetType `yaml:"-"` +} + +type Completion struct { + Type string `yaml:"type,omitempty"` + Command []string `yaml:"command,omitempty"` +} + +type NamedLookup struct { + Type string `yaml:"type,omitempty"` + Command []string `yaml:"command,omitempty"` + Options NamedLookupOptions `yaml:"options"` +} + +type NamedLookupOptions struct { + IDPattern string `yaml:"idPattern,omitempty"` +} + +type TargetType int + +const ( + ParamHeader TargetType = iota + ParamBody + ParamPath + ParamQueryParameter +) + +func (p *Parameter) IsRequired() bool { + return p.Required != nil && *p.Required +} + +func (p *Parameter) AcceptsPipeline() bool { + return p.Pipeline != nil && *p.Pipeline +} + +func (p *Parameter) IsHidden() bool { + return p.Hidden != nil && *p.Hidden +} + +func (p *Parameter) GetDescription() string { + var sb strings.Builder + sb.WriteString(p.Description) + if p.Required != nil && *p.Required { + sb.WriteString(" (required)") + } + if p.Pipeline != nil && *p.Pipeline { + sb.WriteString(" (accepts pipeline)") + } + return sb.String() } func (p *Parameter) GetTargetProperty() string { @@ -117,17 +290,16 @@ func (p *Parameter) GetTargetProperty() string { return p.Name } -func (p *EndPoint) IsCollection() bool { - return strings.EqualFold(p.Method, "GET") && - (p.CollectionProperty != "" || strings.Contains(strings.ToLower(p.Accept), "collection")) -} - -func (p *EndPoint) SupportsTemplates() bool { - return strings.EqualFold(p.Method, "PUT") || strings.EqualFold(p.Method, "POST") -} - -func (p *EndPoint) IsBodyFormData() bool { - return p.BodyContent != nil && p.BodyContent.Type == "formdata" +// Get the first dependent property name defined for the current parameter. +// Return a default value if no dependent property is defined +func (p *Parameter) GetDependentProperty(index int, defaultValue string) string { + if len(p.DependsOn) == 0 { + return defaultValue + } + if index < len(p.DependsOn) { + return p.DependsOn[index] + } + return defaultValue } func (p *Parameter) IsTypeDateTime() bool { diff --git a/internal/run/run.go b/internal/run/run.go index 58fb189e3..ed43672ac 100644 --- a/internal/run/run.go +++ b/internal/run/run.go @@ -2,12 +2,15 @@ package run import ( "bytes" + "errors" "fmt" "io" "os" "os/exec" "path/filepath" "strings" + + "github.com/reubenmiller/go-c8y-cli/v2/internal/utils" ) // Runnable is typically an exec.Cmd or its stub in tests @@ -28,23 +31,26 @@ type cmdWithStderr struct { } func (c cmdWithStderr) Output() ([]byte, error) { - if os.Getenv("DEBUG") != "" { + if isVerbose, _ := utils.IsDebugEnabled(); isVerbose { _ = printArgs(os.Stderr, c.Cmd.Args) } - if c.Cmd.Stderr != nil { - return c.Cmd.Output() - } - errStream := &bytes.Buffer{} - c.Cmd.Stderr = errStream out, err := c.Cmd.Output() - if err != nil { - err = &CmdError{errStream, c.Cmd.Args, err} + if c.Cmd.Stderr != nil || err == nil { + return out, err } - return out, err + cmdErr := &CmdError{ + Args: c.Cmd.Args, + Err: err, + } + var exitError *exec.ExitError + if errors.As(err, &exitError) { + cmdErr.Stderr = bytes.NewBuffer(exitError.Stderr) + } + return out, cmdErr } func (c cmdWithStderr) Run() error { - if os.Getenv("DEBUG") != "" { + if isVerbose, _ := utils.IsDebugEnabled(); isVerbose { _ = printArgs(os.Stderr, c.Cmd.Args) } if c.Cmd.Stderr != nil { @@ -54,16 +60,20 @@ func (c cmdWithStderr) Run() error { c.Cmd.Stderr = errStream err := c.Cmd.Run() if err != nil { - err = &CmdError{errStream, c.Cmd.Args, err} + err = &CmdError{ + Args: c.Cmd.Args, + Err: err, + Stderr: errStream, + } } return err } // CmdError provides more visibility into why an exec.Cmd had failed type CmdError struct { - Stderr *bytes.Buffer Args []string Err error + Stderr *bytes.Buffer } func (e CmdError) Error() string { @@ -74,6 +84,10 @@ func (e CmdError) Error() string { return fmt.Sprintf("%s%s: %s", msg, e.Args[0], e.Err) } +func (e CmdError) Unwrap() error { + return e.Err +} + func printArgs(w io.Writer, args []string) error { if len(args) > 0 { // print commands, but omit the full path to an executable diff --git a/internal/utils/utils.go b/internal/utils/utils.go new file mode 100644 index 000000000..660e12441 --- /dev/null +++ b/internal/utils/utils.go @@ -0,0 +1,24 @@ +package utils + +import "os" + +func IsDebugEnabled() (bool, string) { + debugValue, isDebugSet := os.LookupEnv("GH_DEBUG") + legacyDebugValue := os.Getenv("DEBUG") + + if !isDebugSet { + switch legacyDebugValue { + case "true", "1", "yes", "api": + return true, legacyDebugValue + default: + return false, legacyDebugValue + } + } + + switch debugValue { + case "false", "0", "no", "": + return false, debugValue + default: + return true, debugValue + } +} diff --git a/pkg/c8ybinary/c8ybinary.go b/pkg/c8ybinary/c8ybinary.go index f96df8feb..c2bbdb688 100644 --- a/pkg/c8ybinary/c8ybinary.go +++ b/pkg/c8ybinary/c8ybinary.go @@ -23,6 +23,8 @@ const BarFiller = "[â”â” ]" // const BarFiller = "[██-]" +type ClientFunc func() (*c8y.Client, error) + func CreateBinaryWithProgress(ctx context.Context, client *c8y.Client, path string, filename string, properties interface{}, progress *mpb.Progress) (*c8y.Response, error) { file, err := os.Open(filename) if err != nil { @@ -178,7 +180,7 @@ func NewProxyReader(progress *mpb.Progress, r io.ReadCloser, filename string) (i } // WithBinaryUploadURL uploads an inventory binary and returns the URL to it -func WithBinaryUploadURL(client *c8y.Client, progress *mpb.Progress, opts ...string) flags.GetOption { +func WithBinaryUploadURL(clientFunc ClientFunc, progress *mpb.Progress, opts ...string) flags.GetOption { return func(cmd *cobra.Command, inputIterators *flags.RequestInputIterators) (string, interface{}, error) { src, dst, _ := flags.UnpackGetterOptions("%s", opts...) @@ -214,6 +216,11 @@ func WithBinaryUploadURL(client *c8y.Client, progress *mpb.Progress, opts ...str return "", nil, err } + client, err := clientFunc() + if err != nil { + return "", nil, err + } + mo, _, err := client.Inventory.CreateBinary(context.Background(), binaryFile, func(r *http.Request) (*http.Request, error) { if bar != nil { r.Body = bar.ProxyReader(r.Body) diff --git a/pkg/c8yfetcher/agentFetcher.manual.go b/pkg/c8yfetcher/agentFetcher.manual.go index ef5d764e4..1acd04b4f 100644 --- a/pkg/c8yfetcher/agentFetcher.manual.go +++ b/pkg/c8yfetcher/agentFetcher.manual.go @@ -4,23 +4,25 @@ import ( "fmt" "github.com/pkg/errors" + "github.com/reubenmiller/go-c8y-cli/v2/pkg/cmdutil" "github.com/reubenmiller/go-c8y/pkg/c8y" ) type AgentFetcher struct { - client *c8y.Client - *DefaultFetcher + *CumulocityFetcher } -func NewAgentFetcher(client *c8y.Client) *AgentFetcher { +func NewAgentFetcher(factory *cmdutil.Factory) *AgentFetcher { return &AgentFetcher{ - client: client, + CumulocityFetcher: &CumulocityFetcher{ + factory: factory, + }, } } func (f *AgentFetcher) getByID(id string) ([]fetcherResultSet, error) { - mo, resp, err := f.client.Inventory.GetManagedObject( - WithDisabledDryRunContext(f.client), + mo, resp, err := f.Client().Inventory.GetManagedObject( + WithDisabledDryRunContext(f.Client()), id, nil, ) @@ -39,8 +41,8 @@ func (f *AgentFetcher) getByID(id string) ([]fetcherResultSet, error) { } func (f *AgentFetcher) getByName(name string) ([]fetcherResultSet, error) { - mcol, _, err := f.client.Inventory.GetManagedObjects( - WithDisabledDryRunContext(f.client), + mcol, _, err := f.Client().Inventory.GetManagedObjects( + WithDisabledDryRunContext(f.Client()), &c8y.ManagedObjectOptions{ // fmt.Sprintf("$filter=%s+$orderby=name", filter) Query: fmt.Sprintf("$filter=has(com_cumulocity_model_Agent) and name eq '%s' $orderby=name", name), diff --git a/pkg/c8yfetcher/applicationFetcher.go b/pkg/c8yfetcher/applicationFetcher.go index d83bae630..a8d3c4313 100644 --- a/pkg/c8yfetcher/applicationFetcher.go +++ b/pkg/c8yfetcher/applicationFetcher.go @@ -4,23 +4,25 @@ import ( "regexp" "github.com/pkg/errors" + "github.com/reubenmiller/go-c8y-cli/v2/pkg/cmdutil" "github.com/reubenmiller/go-c8y/pkg/c8y" ) type ApplicationFetcher struct { - client *c8y.Client - *DefaultFetcher + *CumulocityFetcher } -func NewApplicationFetcher(client *c8y.Client) *ApplicationFetcher { +func NewApplicationFetcher(factory *cmdutil.Factory) *ApplicationFetcher { return &ApplicationFetcher{ - client: client, + CumulocityFetcher: &CumulocityFetcher{ + factory: factory, + }, } } func (f *ApplicationFetcher) getByID(id string) ([]fetcherResultSet, error) { - app, resp, err := f.client.Application.GetApplication( - WithDisabledDryRunContext(f.client), + app, resp, err := f.Client().Application.GetApplication( + WithDisabledDryRunContext(f.Client()), id, ) @@ -39,8 +41,8 @@ func (f *ApplicationFetcher) getByID(id string) ([]fetcherResultSet, error) { // getByName returns applications matching a given using regular expression func (f *ApplicationFetcher) getByName(name string) ([]fetcherResultSet, error) { - col, _, err := f.client.Application.GetApplications( - WithDisabledDryRunContext(f.client), + col, _, err := f.Client().Application.GetApplications( + WithDisabledDryRunContext(f.Client()), &c8y.ApplicationOptions{ PaginationOptions: *c8y.NewPaginationOptions(2000), }, diff --git a/pkg/c8yfetcher/c8yfetcher.go b/pkg/c8yfetcher/c8yfetcher.go index 470bffb0b..9e21a6a5a 100644 --- a/pkg/c8yfetcher/c8yfetcher.go +++ b/pkg/c8yfetcher/c8yfetcher.go @@ -9,6 +9,7 @@ import ( "strings" "github.com/reubenmiller/go-c8y-cli/v2/pkg/cmderrors" + "github.com/reubenmiller/go-c8y-cli/v2/pkg/cmdutil" "github.com/reubenmiller/go-c8y-cli/v2/pkg/flags" "github.com/reubenmiller/go-c8y-cli/v2/pkg/iterator" "github.com/reubenmiller/go-c8y/pkg/c8y" @@ -204,7 +205,7 @@ func GetFetchedResultsAsString(refs []entityReference) (results []string, invali type EntityIterator struct { Fetcher EntityFetcher - Client *c8y.Client + Factory *cmdutil.Factory valueIterator iterator.Iterator GetID bool UseSelfLink bool @@ -214,10 +215,10 @@ type EntityIterator struct { } // NewReferenceByNameIterator create a new iterator which can look up values by their id or names -func NewReferenceByNameIterator(fetcher EntityFetcher, c8yClient *c8y.Client, valueIterator iterator.Iterator, minimumMatches int, overrideValue iterator.Iterator, format string) *EntityIterator { +func NewReferenceByNameIterator(fetcher EntityFetcher, factory *cmdutil.Factory, valueIterator iterator.Iterator, minimumMatches int, overrideValue iterator.Iterator, format string) *EntityIterator { return &EntityIterator{ Fetcher: fetcher, - Client: c8yClient, + Factory: factory, valueIterator: valueIterator, GetID: false, MinimumMatches: minimumMatches, @@ -313,10 +314,11 @@ func WithIDSlice(args []string, opts ...string) flags.GetOption { // check for arguments which could override the value values, err := cmd.Flags().GetStringSlice(src) if err != nil { - singleValue, err := cmd.Flags().GetString(src) - if err != nil { - return "", "", err + singleValue, singleErr := cmd.Flags().GetString(src) + if singleErr != nil { + return "", "", singleErr } + err = nil values = []string{singleValue} } @@ -358,18 +360,18 @@ func WithIDSlice(args []string, opts ...string) flags.GetOption { } // WithReferenceByName adds support for looking up values by name via cli args -func WithReferenceByName(client *c8y.Client, fetcher EntityFetcher, args []string, opts ...string) flags.GetOption { +func WithReferenceByName(factory *cmdutil.Factory, fetcher EntityFetcher, args []string, opts ...string) flags.GetOption { return func(cmd *cobra.Command, inputIterators *flags.RequestInputIterators) (string, interface{}, error) { - src, dst, format := flags.UnpackGetterOptions("", opts...) // check for arguments which could override the value values, err := cmd.Flags().GetStringSlice(src) if err != nil { - singleValue, err := cmd.Flags().GetString(src) - if err != nil { - return "", "", err + singleValue, singleErr := cmd.Flags().GetString(src) + if singleErr != nil { + return "", "", singleErr } + err = nil values = []string{singleValue} } @@ -402,7 +404,7 @@ func WithReferenceByName(client *c8y.Client, fetcher EntityFetcher, args []strin if inputIterators.PipeOptions.Required { minMatches = 1 } - iter := NewReferenceByNameIterator(fetcher, client, pipeIter, minMatches, overrideValue, format) + iter := NewReferenceByNameIterator(fetcher, factory, pipeIter, minMatches, overrideValue, format) return inputIterators.PipeOptions.Property, iter, nil } @@ -444,7 +446,7 @@ func WithReferenceByName(client *c8y.Client, fetcher EntityFetcher, args []strin } // WithSelfReferenceByName adds support for looking up values by name via cli args -func WithSelfReferenceByName(client *c8y.Client, fetcher EntityFetcher, args []string, opts ...string) flags.GetOption { +func WithSelfReferenceByName(factory *cmdutil.Factory, fetcher EntityFetcher, args []string, opts ...string) flags.GetOption { return func(cmd *cobra.Command, inputIterators *flags.RequestInputIterators) (string, interface{}, error) { src, dst, format := flags.UnpackGetterOptions("", opts...) @@ -452,10 +454,11 @@ func WithSelfReferenceByName(client *c8y.Client, fetcher EntityFetcher, args []s // check for arguments which could override the value values, err := cmd.Flags().GetStringSlice(src) if err != nil { - singleValue, err := cmd.Flags().GetString(src) - if err != nil { - return "", "", err + singleValue, singleErr := cmd.Flags().GetString(src) + if singleErr != nil { + return "", "", singleErr } + err = nil values = []string{singleValue} } @@ -463,7 +466,7 @@ func WithSelfReferenceByName(client *c8y.Client, fetcher EntityFetcher, args []s var overrideValue iterator.Iterator if len(values) > 0 { - overrideValue = iterator.NewSliceIterator(values) + overrideValue = iterator.NewSliceIterator(values, format) } if inputIterators != nil && inputIterators.PipeOptions.Name == src { @@ -481,7 +484,7 @@ func WithSelfReferenceByName(client *c8y.Client, fetcher EntityFetcher, args []s if inputIterators.PipeOptions.Required { minMatches = 1 } - iter := NewReferenceByNameIterator(fetcher, client, pipeIter, minMatches, overrideValue, format) + iter := NewReferenceByNameIterator(fetcher, factory, pipeIter, minMatches, overrideValue, format) iter.UseSelfLink = true iter.GetID = true return inputIterators.PipeOptions.Property, iter, nil @@ -533,9 +536,13 @@ func WithSelfReferenceByName(client *c8y.Client, fetcher EntityFetcher, args []s } // WithManagedObjectPropertyFirstMatch add reference by name matching using a fetcher via cli args. Only the first match will be used -func WithManagedObjectPropertyFirstMatch(client *c8y.Client, fetcher EntityFetcher, args []string, property string, opts ...string) flags.GetOption { +func WithManagedObjectPropertyFirstMatch(factory *cmdutil.Factory, fetcher EntityFetcher, args []string, property string, opts ...string) flags.GetOption { return func(cmd *cobra.Command, inputIterators *flags.RequestInputIterators) (string, interface{}, error) { - opt := WithReferenceByName(client, fetcher, args, opts...) + client, err := factory.Client() + if err != nil { + return "", nil, err + } + opt := WithReferenceByName(factory, fetcher, args, opts...) name, values, err := opt(cmd, inputIterators) if name == "" { @@ -570,9 +577,9 @@ func WithManagedObjectPropertyFirstMatch(client *c8y.Client, fetcher EntityFetch } // WithReferenceByNameFirstMatch add reference by name matching using a fetcher via cli args. Only the first match will be used -func WithReferenceByNameFirstMatch(client *c8y.Client, fetcher EntityFetcher, args []string, opts ...string) flags.GetOption { +func WithReferenceByNameFirstMatch(factory *cmdutil.Factory, fetcher EntityFetcher, args []string, opts ...string) flags.GetOption { return func(cmd *cobra.Command, inputIterators *flags.RequestInputIterators) (string, interface{}, error) { - opt := WithReferenceByName(client, fetcher, args, opts...) + opt := WithReferenceByName(factory, fetcher, args, opts...) name, values, err := opt(cmd, inputIterators) if name == "" { @@ -599,9 +606,9 @@ func WithReferenceByNameFirstMatch(client *c8y.Client, fetcher EntityFetcher, ar } // WithSelfReferenceByNameFirstMatch add reference by name matching using a fetcher via cli args. Only the first match will be used -func WithSelfReferenceByNameFirstMatch(client *c8y.Client, fetcher EntityFetcher, args []string, opts ...string) flags.GetOption { +func WithSelfReferenceByNameFirstMatch(factory *cmdutil.Factory, fetcher EntityFetcher, args []string, opts ...string) flags.GetOption { return func(cmd *cobra.Command, inputIterators *flags.RequestInputIterators) (string, interface{}, error) { - opt := WithSelfReferenceByName(client, fetcher, args, opts...) + opt := WithSelfReferenceByName(factory, fetcher, args, opts...) name, values, err := opt(cmd, inputIterators) if name == "" { @@ -628,73 +635,76 @@ func WithSelfReferenceByNameFirstMatch(client *c8y.Client, fetcher EntityFetcher } // WithDeviceByNameFirstMatch add reference by name matching for devices via cli args. Only the first match will be used -func WithDeviceByNameFirstMatch(client *c8y.Client, args []string, opts ...string) flags.GetOption { +func WithDeviceByNameFirstMatch(factory *cmdutil.Factory, args []string, opts ...string) flags.GetOption { return func(cmd *cobra.Command, inputIterators *flags.RequestInputIterators) (string, interface{}, error) { - opt := WithReferenceByNameFirstMatch(client, NewDeviceFetcher(client), args, opts...) + opt := WithReferenceByNameFirstMatch(factory, NewDeviceFetcher(factory), args, opts...) return opt(cmd, inputIterators) } } // WithApplicationByNameFirstMatch add reference by name matching for applications via cli args. Only the first match will be used -func WithApplicationByNameFirstMatch(client *c8y.Client, args []string, opts ...string) flags.GetOption { +func WithApplicationByNameFirstMatch(factory *cmdutil.Factory, args []string, opts ...string) flags.GetOption { return func(cmd *cobra.Command, inputIterators *flags.RequestInputIterators) (string, interface{}, error) { - opt := WithReferenceByNameFirstMatch(client, NewApplicationFetcher(client), args, opts...) + opt := WithReferenceByNameFirstMatch(factory, NewApplicationFetcher(factory), args, opts...) return opt(cmd, inputIterators) } } // WithHostedApplicationByNameFirstMatch add reference by name matching for hosted (web) applications via cli args. Only the first match will be used -func WithHostedApplicationByNameFirstMatch(client *c8y.Client, args []string, opts ...string) flags.GetOption { +func WithHostedApplicationByNameFirstMatch(factory *cmdutil.Factory, args []string, opts ...string) flags.GetOption { return func(cmd *cobra.Command, inputIterators *flags.RequestInputIterators) (string, interface{}, error) { - opt := WithReferenceByNameFirstMatch(client, NewHostedApplicationFetcher(client), args, opts...) + opt := WithReferenceByNameFirstMatch(factory, NewHostedApplicationFetcher(factory, false), args, opts...) return opt(cmd, inputIterators) } } // WithMicroserviceByNameFirstMatch add reference by name matching for microservices via cli args. Only the first match will be used -func WithMicroserviceByNameFirstMatch(client *c8y.Client, args []string, opts ...string) flags.GetOption { +func WithMicroserviceByNameFirstMatch(factory *cmdutil.Factory, args []string, opts ...string) flags.GetOption { return func(cmd *cobra.Command, inputIterators *flags.RequestInputIterators) (string, interface{}, error) { - opt := WithReferenceByNameFirstMatch(client, NewMicroserviceFetcher(client), args, opts...) + opt := WithReferenceByNameFirstMatch(factory, NewMicroserviceFetcher(factory), args, opts...) return opt(cmd, inputIterators) } } // WithAgentByNameFirstMatch add reference by name matching for agents via cli args. Only the first match will be used -func WithAgentByNameFirstMatch(client *c8y.Client, args []string, opts ...string) flags.GetOption { +func WithAgentByNameFirstMatch(factory *cmdutil.Factory, args []string, opts ...string) flags.GetOption { return func(cmd *cobra.Command, inputIterators *flags.RequestInputIterators) (string, interface{}, error) { - opt := WithReferenceByNameFirstMatch(client, NewAgentFetcher(client), args, opts...) + opt := WithReferenceByNameFirstMatch(factory, NewAgentFetcher(factory), args, opts...) return opt(cmd, inputIterators) } } // WithDeviceGroupByNameFirstMatch add reference by name matching for device groups via cli args. Only the first match will be used -func WithDeviceGroupByNameFirstMatch(client *c8y.Client, args []string, opts ...string) flags.GetOption { +func WithDeviceGroupByNameFirstMatch(factory *cmdutil.Factory, args []string, opts ...string) flags.GetOption { return func(cmd *cobra.Command, inputIterators *flags.RequestInputIterators) (string, interface{}, error) { - opt := WithReferenceByNameFirstMatch(client, NewDeviceGroupFetcher(client), args, opts...) + opt := WithReferenceByNameFirstMatch(factory, NewDeviceGroupFetcher(factory), args, opts...) return opt(cmd, inputIterators) } } // WithSmartGroupByNameFirstMatch add reference by name matching for smart groups via cli args. Only the first match will be used -func WithSmartGroupByNameFirstMatch(client *c8y.Client, args []string, opts ...string) flags.GetOption { +func WithSmartGroupByNameFirstMatch(factory *cmdutil.Factory, args []string, opts ...string) flags.GetOption { return func(cmd *cobra.Command, inputIterators *flags.RequestInputIterators) (string, interface{}, error) { - opt := WithReferenceByNameFirstMatch(client, NewSmartGroupFetcher(client), args, opts...) + opt := WithReferenceByNameFirstMatch(factory, NewSmartGroupFetcher(factory), args, opts...) return opt(cmd, inputIterators) } } // WithSoftwareByNameFirstMatch add reference by name matching for software via cli args. Only the first match will be used -func WithSoftwareByNameFirstMatch(client *c8y.Client, args []string, opts ...string) flags.GetOption { +func WithSoftwareByNameFirstMatch(factory *cmdutil.Factory, args []string, opts ...string) flags.GetOption { return func(cmd *cobra.Command, inputIterators *flags.RequestInputIterators) (string, interface{}, error) { - opt := WithReferenceByNameFirstMatch(client, NewSoftwareFetcher(client), args, opts...) + opt := WithReferenceByNameFirstMatch(factory, NewSoftwareFetcher(factory), args, opts...) return opt(cmd, inputIterators) } } // WithSoftwareVersionData adds software information (name, version and url) -func WithSoftwareVersionData(client *c8y.Client, flagSoftware, flagVersion, flagURL string, args []string, opts ...string) flags.GetOption { +func WithSoftwareVersionData(factory *cmdutil.Factory, flagSoftware, flagVersion, flagURL string, args []string, opts ...string) flags.GetOption { return func(cmd *cobra.Command, inputIterators *flags.RequestInputIterators) (string, interface{}, error) { - var err error + client, err := factory.Client() + if err != nil { + return "", nil, err + } software := "" if v, err := flags.GetFlagStringValues(cmd, flagSoftware); err == nil && len(v) > 0 { software = v[0] @@ -758,66 +768,69 @@ func WithSoftwareVersionData(client *c8y.Client, flagSoftware, flagVersion, flag } // WithSoftwareVersionUrl add software version url -func WithSoftwareVersionUrlByNameFirstMatch(client *c8y.Client, args []string, opts ...string) flags.GetOption { +func WithSoftwareVersionUrlByNameFirstMatch(factory *cmdutil.Factory, softwareFlag string, args []string, opts ...string) flags.GetOption { return func(cmd *cobra.Command, inputIterators *flags.RequestInputIterators) (string, interface{}, error) { software := "" - if v, err := flags.GetFlagStringValues(cmd, "software"); err == nil && len(v) > 0 { + if v, err := flags.GetFlagStringValues(cmd, softwareFlag); err == nil && len(v) > 0 { software = v[0] } - opt := WithManagedObjectPropertyFirstMatch(client, NewSoftwareVersionFetcher(client, software), args, "c8y_Software.url", opts...) + opt := WithManagedObjectPropertyFirstMatch(factory, NewSoftwareVersionFetcher(factory, software), args, "c8y_Software.url", opts...) return opt(cmd, inputIterators) } } // WithSoftwareVersionByNameFirstMatch add reference by name matching for software version via cli args. Only the first match will be used -func WithSoftwareVersionByNameFirstMatch(client *c8y.Client, args []string, opts ...string) flags.GetOption { +func WithSoftwareVersionByNameFirstMatch(factory *cmdutil.Factory, softwareFlag string, args []string, opts ...string) flags.GetOption { return func(cmd *cobra.Command, inputIterators *flags.RequestInputIterators) (string, interface{}, error) { software := "" - if v, err := cmd.Flags().GetStringSlice("software"); err == nil && len(v) > 0 { + if v, err := cmd.Flags().GetStringSlice(softwareFlag); err == nil && len(v) > 0 { software = v[0] } - opt := WithReferenceByNameFirstMatch(client, NewSoftwareVersionFetcher(client, software), args, opts...) + opt := WithReferenceByNameFirstMatch(factory, NewSoftwareVersionFetcher(factory, software), args, opts...) return opt(cmd, inputIterators) } } // WithSoftwareVersionByNameFirstMatch add reference by name matching for software version via cli args. Only the first match will be used -func WithDeviceServiceByNameFirstMatch(client *c8y.Client, args []string, opts ...string) flags.GetOption { +func WithDeviceServiceByNameFirstMatch(factory *cmdutil.Factory, flagDevice string, args []string, opts ...string) flags.GetOption { return func(cmd *cobra.Command, inputIterators *flags.RequestInputIterators) (string, interface{}, error) { device := "" - if v, err := cmd.Flags().GetStringSlice("device"); err == nil && len(v) > 0 { + if v, err := cmd.Flags().GetStringSlice(flagDevice); err == nil && len(v) > 0 { device = v[0] } - opt := WithReferenceByNameFirstMatch(client, NewDeviceServiceFetcher(client, device), args, opts...) + opt := WithReferenceByNameFirstMatch(factory, NewDeviceServiceFetcher(factory, device), args, opts...) return opt(cmd, inputIterators) } } // WithFirmwareByNameFirstMatch add reference by name matching for firmware via cli args. Only the first match will be used -func WithFirmwareByNameFirstMatch(client *c8y.Client, args []string, opts ...string) flags.GetOption { +func WithFirmwareByNameFirstMatch(factory *cmdutil.Factory, args []string, opts ...string) flags.GetOption { return func(cmd *cobra.Command, inputIterators *flags.RequestInputIterators) (string, interface{}, error) { - opt := WithReferenceByNameFirstMatch(client, NewFirmwareFetcher(client), args, opts...) + opt := WithReferenceByNameFirstMatch(factory, NewFirmwareFetcher(factory), args, opts...) return opt(cmd, inputIterators) } } // WithFirmwareVersionByNameFirstMatch add reference by name matching for firmware version via cli args. Only the first match will be used -func WithFirmwareVersionByNameFirstMatch(client *c8y.Client, args []string, opts ...string) flags.GetOption { +func WithFirmwareVersionByNameFirstMatch(factory *cmdutil.Factory, flagFirmware string, args []string, opts ...string) flags.GetOption { return func(cmd *cobra.Command, inputIterators *flags.RequestInputIterators) (string, interface{}, error) { firmware := "" // Note: Lookup of firmware does not work if "firmware" is piped input - if v, err := cmd.Flags().GetStringSlice("firmware"); err == nil && len(v) > 0 { + if v, err := cmd.Flags().GetStringSlice(flagFirmware); err == nil && len(v) > 0 { firmware = v[0] } - opt := WithReferenceByNameFirstMatch(client, NewFirmwareVersionFetcher(client, firmware, false), args, opts...) + opt := WithReferenceByNameFirstMatch(factory, NewFirmwareVersionFetcher(factory, firmware, false), args, opts...) return opt(cmd, inputIterators) } } // WithFirmwareVersionData adds firmware information (name, version and url) -func WithFirmwareVersionData(client *c8y.Client, flagFirmware, flagVersion, flagURL string, args []string, opts ...string) flags.GetOption { +func WithFirmwareVersionData(factory *cmdutil.Factory, flagFirmware, flagVersion, flagURL string, args []string, opts ...string) flags.GetOption { return func(cmd *cobra.Command, inputIterators *flags.RequestInputIterators) (string, interface{}, error) { - var err error + client, err := factory.Client() + if err != nil { + return "", nil, err + } firmware := "" if v, err := flags.GetFlagStringValues(cmd, flagFirmware); err == nil && len(v) > 0 { firmware = v[0] @@ -881,22 +894,25 @@ func WithFirmwareVersionData(client *c8y.Client, flagFirmware, flagVersion, flag } // WithFirmwarePatchByNameFirstMatch add reference by name matching for firmware version via cli args. Only the first match will be used -func WithFirmwarePatchByNameFirstMatch(client *c8y.Client, args []string, opts ...string) flags.GetOption { +func WithFirmwarePatchByNameFirstMatch(factory *cmdutil.Factory, firmwareFlag string, args []string, opts ...string) flags.GetOption { return func(cmd *cobra.Command, inputIterators *flags.RequestInputIterators) (string, interface{}, error) { firmware := "" // Note: Lookup of firmware does not work if "firmware" is piped input - if v, err := cmd.Flags().GetStringSlice("firmware"); err == nil && len(v) > 0 { + if v, err := cmd.Flags().GetStringSlice(firmwareFlag); err == nil && len(v) > 0 { firmware = v[0] } - opt := WithReferenceByNameFirstMatch(client, NewFirmwareVersionFetcher(client, firmware, true), args, opts...) + opt := WithReferenceByNameFirstMatch(factory, NewFirmwareVersionFetcher(factory, firmware, true), args, opts...) return opt(cmd, inputIterators) } } // WithConfigurationFileData adds configuration information (type, url etc.) -func WithConfigurationFileData(client *c8y.Client, flagConfiguration, flagConfigurationType, flagURL string, args []string, opts ...string) flags.GetOption { +func WithConfigurationFileData(factory *cmdutil.Factory, flagConfiguration, flagConfigurationType, flagURL string, args []string, opts ...string) flags.GetOption { return func(cmd *cobra.Command, inputIterators *flags.RequestInputIterators) (string, interface{}, error) { - var err error + client, err := factory.Client() + if err != nil { + return "", nil, err + } configuration := "" if v, err := flags.GetFlagStringValues(cmd, flagConfiguration); err == nil && len(v) > 0 { configuration = v[0] @@ -971,65 +987,73 @@ func WithConfigurationFileData(client *c8y.Client, flagConfiguration, flagConfig } // WithConfigurationByNameFirstMatch add reference by name matching for configuration via cli args. Only the first match will be used -func WithConfigurationByNameFirstMatch(client *c8y.Client, args []string, opts ...string) flags.GetOption { +func WithConfigurationByNameFirstMatch(factory *cmdutil.Factory, args []string, opts ...string) flags.GetOption { return func(cmd *cobra.Command, inputIterators *flags.RequestInputIterators) (string, interface{}, error) { - opt := WithReferenceByNameFirstMatch(client, NewConfigurationFetcher(client), args, opts...) + opt := WithReferenceByNameFirstMatch(factory, NewConfigurationFetcher(factory), args, opts...) return opt(cmd, inputIterators) } } // WithDeviceProfileByNameFirstMatch add reference by name matching for device profile via cli args. Only the first match will be used -func WithDeviceProfileByNameFirstMatch(client *c8y.Client, args []string, opts ...string) flags.GetOption { +func WithDeviceProfileByNameFirstMatch(factory *cmdutil.Factory, args []string, opts ...string) flags.GetOption { return func(cmd *cobra.Command, inputIterators *flags.RequestInputIterators) (string, interface{}, error) { - opt := WithReferenceByNameFirstMatch(client, NewDeviceProfileFetcher(client), args, opts...) + opt := WithReferenceByNameFirstMatch(factory, NewDeviceProfileFetcher(factory), args, opts...) return opt(cmd, inputIterators) } } // WithUserByNameFirstMatch add reference by name matching for users via cli args. Only the first match will be used -func WithUserByNameFirstMatch(client *c8y.Client, args []string, opts ...string) flags.GetOption { +func WithUserByNameFirstMatch(factory *cmdutil.Factory, args []string, opts ...string) flags.GetOption { return func(cmd *cobra.Command, inputIterators *flags.RequestInputIterators) (string, interface{}, error) { - opt := WithReferenceByNameFirstMatch(client, NewUserFetcher(client), args, opts...) + opt := WithReferenceByNameFirstMatch(factory, NewUserFetcher(factory), args, opts...) return opt(cmd, inputIterators) } } // WithUserSelfByNameFirstMatch add reference by name matching for users' self link via cli args. Only the first match will be used -func WithUserSelfByNameFirstMatch(client *c8y.Client, args []string, opts ...string) flags.GetOption { +func WithUserSelfByNameFirstMatch(factory *cmdutil.Factory, args []string, opts ...string) flags.GetOption { return func(cmd *cobra.Command, inputIterators *flags.RequestInputIterators) (string, interface{}, error) { - opt := WithSelfReferenceByNameFirstMatch(client, NewUserFetcher(client), args, opts...) + opt := WithSelfReferenceByNameFirstMatch(factory, NewUserFetcher(factory), args, opts...) return opt(cmd, inputIterators) } } // WithRoleSelfByNameFirstMatch add reference by name matching for roles' self link via cli args. Only the first match will be used -func WithRoleSelfByNameFirstMatch(client *c8y.Client, args []string, opts ...string) flags.GetOption { +func WithRoleSelfByNameFirstMatch(factory *cmdutil.Factory, args []string, opts ...string) flags.GetOption { return func(cmd *cobra.Command, inputIterators *flags.RequestInputIterators) (string, interface{}, error) { - opt := WithSelfReferenceByNameFirstMatch(client, NewRoleFetcher(client), args, opts...) + opt := WithSelfReferenceByNameFirstMatch(factory, NewRoleFetcher(factory), args, opts...) return opt(cmd, inputIterators) } } // WithRoleByNameFirstMatch add reference by name matching for roles via cli args. Only the first match will be used -func WithRoleByNameFirstMatch(client *c8y.Client, args []string, opts ...string) flags.GetOption { +func WithRoleByNameFirstMatch(factory *cmdutil.Factory, args []string, opts ...string) flags.GetOption { return func(cmd *cobra.Command, inputIterators *flags.RequestInputIterators) (string, interface{}, error) { - opt := WithReferenceByNameFirstMatch(client, NewRoleFetcher(client), args, opts...) + opt := WithReferenceByNameFirstMatch(factory, NewRoleFetcher(factory), args, opts...) return opt(cmd, inputIterators) } } // WithUserGroupByNameFirstMatch add reference by name matching for user groups via cli args. Only the first match will be used -func WithUserGroupByNameFirstMatch(client *c8y.Client, args []string, opts ...string) flags.GetOption { +func WithUserGroupByNameFirstMatch(factory *cmdutil.Factory, args []string, opts ...string) flags.GetOption { return func(cmd *cobra.Command, inputIterators *flags.RequestInputIterators) (string, interface{}, error) { - opt := WithReferenceByNameFirstMatch(client, NewUserGroupFetcher(client), args, opts...) + opt := WithReferenceByNameFirstMatch(factory, NewUserGroupFetcher(factory), args, opts...) return opt(cmd, inputIterators) } } // WithCertificateByNameFirstMatch add reference by name matching for trusted device certificate via cli args. Only the first match will be used -func WithCertificateByNameFirstMatch(client *c8y.Client, args []string, opts ...string) flags.GetOption { +func WithCertificateByNameFirstMatch(factory *cmdutil.Factory, args []string, opts ...string) flags.GetOption { + return func(cmd *cobra.Command, inputIterators *flags.RequestInputIterators) (string, interface{}, error) { + opt := WithReferenceByNameFirstMatch(factory, NewDeviceCertificateFetcher(factory), args, opts...) + return opt(cmd, inputIterators) + } +} + +// WithSoftwareVersionByNameFirstMatch add reference by name matching for software version via cli args. Only the first match will be used +func WithExternalCommandByNameFirstMatch(factory *cmdutil.Factory, args []string, externalCommand []string, idPattern string, opts ...string) flags.GetOption { return func(cmd *cobra.Command, inputIterators *flags.RequestInputIterators) (string, interface{}, error) { - opt := WithReferenceByNameFirstMatch(client, NewDeviceCertificateFetcher(client), args, opts...) + opt := WithReferenceByNameFirstMatch(factory, NewExternalFetcher(externalCommand, idPattern), args, opts...) return opt(cmd, inputIterators) } } diff --git a/pkg/c8yfetcher/configurationFetcher.go b/pkg/c8yfetcher/configurationFetcher.go index d04a7aa51..d3f028b78 100644 --- a/pkg/c8yfetcher/configurationFetcher.go +++ b/pkg/c8yfetcher/configurationFetcher.go @@ -3,17 +3,19 @@ package c8yfetcher import ( "fmt" - "github.com/reubenmiller/go-c8y/pkg/c8y" + "github.com/reubenmiller/go-c8y-cli/v2/pkg/cmdutil" ) type ConfigurationFetcher struct { *ManagedObjectFetcher } -func NewConfigurationFetcher(client *c8y.Client) *ConfigurationFetcher { +func NewConfigurationFetcher(factory *cmdutil.Factory) *ConfigurationFetcher { return &ConfigurationFetcher{ ManagedObjectFetcher: &ManagedObjectFetcher{ - client: client, + CumulocityFetcher: &CumulocityFetcher{ + factory: factory, + }, Query: func(s string) string { return fmt.Sprintf("(type eq 'c8y_ConfigurationDump') and name eq '%s'", s) }, diff --git a/pkg/c8yfetcher/cumulocityFetcher.go b/pkg/c8yfetcher/cumulocityFetcher.go new file mode 100644 index 000000000..c8afcbe14 --- /dev/null +++ b/pkg/c8yfetcher/cumulocityFetcher.go @@ -0,0 +1,19 @@ +package c8yfetcher + +import ( + "github.com/reubenmiller/go-c8y-cli/v2/pkg/cmdutil" + "github.com/reubenmiller/go-c8y/pkg/c8y" +) + +type CumulocityFetcher struct { + factory *cmdutil.Factory + *DefaultFetcher +} + +func (f *CumulocityFetcher) Client() *c8y.Client { + client, err := f.factory.Client() + if err != nil { + panic(err) + } + return client +} diff --git a/pkg/c8yfetcher/deviceCertificateFetcher.go b/pkg/c8yfetcher/deviceCertificateFetcher.go index 8e8a46274..70fe9bea3 100644 --- a/pkg/c8yfetcher/deviceCertificateFetcher.go +++ b/pkg/c8yfetcher/deviceCertificateFetcher.go @@ -4,18 +4,21 @@ import ( "strings" "github.com/pkg/errors" + "github.com/reubenmiller/go-c8y-cli/v2/pkg/cmdutil" "github.com/reubenmiller/go-c8y-cli/v2/pkg/matcher" "github.com/reubenmiller/go-c8y/pkg/c8y" ) type DeviceCertificateFetcher struct { - client *c8y.Client + *CumulocityFetcher *IDNameFetcher } -func NewDeviceCertificateFetcher(client *c8y.Client) *DeviceCertificateFetcher { +func NewDeviceCertificateFetcher(factory *cmdutil.Factory) *DeviceCertificateFetcher { return &DeviceCertificateFetcher{ - client: client, + CumulocityFetcher: &CumulocityFetcher{ + factory: factory, + }, } } @@ -32,8 +35,8 @@ func (f *DeviceCertificateFetcher) IsID(id string) bool { } func (f *DeviceCertificateFetcher) getByID(id string) ([]fetcherResultSet, error) { - cert, resp, err := f.client.DeviceCertificate.GetCertificate( - WithDisabledDryRunContext(f.client), + cert, resp, err := f.Client().DeviceCertificate.GetCertificate( + WithDisabledDryRunContext(f.Client()), id, ) @@ -53,8 +56,8 @@ func (f *DeviceCertificateFetcher) getByID(id string) ([]fetcherResultSet, error func (f *DeviceCertificateFetcher) getByName(name string) ([]fetcherResultSet, error) { // check if already resolved, so we can save a lookup - col, _, err := f.client.DeviceCertificate.GetCertificates( - WithDisabledDryRunContext(f.client), + col, _, err := f.Client().DeviceCertificate.GetCertificates( + WithDisabledDryRunContext(f.Client()), &c8y.DeviceCertificateCollectionOptions{ PaginationOptions: *c8y.NewPaginationOptions(100), }, diff --git a/pkg/c8yfetcher/deviceFetcher.go b/pkg/c8yfetcher/deviceFetcher.go index afa71fae1..20b45ed15 100644 --- a/pkg/c8yfetcher/deviceFetcher.go +++ b/pkg/c8yfetcher/deviceFetcher.go @@ -2,23 +2,25 @@ package c8yfetcher import ( "github.com/pkg/errors" + "github.com/reubenmiller/go-c8y-cli/v2/pkg/cmdutil" "github.com/reubenmiller/go-c8y/pkg/c8y" ) type DeviceFetcher struct { - client *c8y.Client - *DefaultFetcher + *CumulocityFetcher } -func NewDeviceFetcher(client *c8y.Client) *DeviceFetcher { +func NewDeviceFetcher(factory *cmdutil.Factory) *DeviceFetcher { return &DeviceFetcher{ - client: client, + CumulocityFetcher: &CumulocityFetcher{ + factory: factory, + }, } } func (f *DeviceFetcher) getByID(id string) ([]fetcherResultSet, error) { - mo, resp, err := f.client.Inventory.GetManagedObject( - WithDisabledDryRunContext(f.client), + mo, resp, err := f.Client().Inventory.GetManagedObject( + WithDisabledDryRunContext(f.Client()), id, nil, ) @@ -37,8 +39,8 @@ func (f *DeviceFetcher) getByID(id string) ([]fetcherResultSet, error) { } func (f *DeviceFetcher) getByName(name string) ([]fetcherResultSet, error) { - mcol, _, err := f.client.Inventory.GetDevicesByName( - WithDisabledDryRunContext(f.client), + mcol, _, err := f.Client().Inventory.GetDevicesByName( + WithDisabledDryRunContext(f.Client()), name, c8y.NewPaginationOptions(5), ) diff --git a/pkg/c8yfetcher/deviceGroupFetcher.go b/pkg/c8yfetcher/deviceGroupFetcher.go index 9c767f1a8..d3ddb5a84 100644 --- a/pkg/c8yfetcher/deviceGroupFetcher.go +++ b/pkg/c8yfetcher/deviceGroupFetcher.go @@ -4,23 +4,25 @@ import ( "fmt" "github.com/pkg/errors" + "github.com/reubenmiller/go-c8y-cli/v2/pkg/cmdutil" "github.com/reubenmiller/go-c8y/pkg/c8y" ) type DeviceGroupFetcher struct { - client *c8y.Client - *DefaultFetcher + *CumulocityFetcher } -func NewDeviceGroupFetcher(client *c8y.Client) *DeviceGroupFetcher { +func NewDeviceGroupFetcher(factory *cmdutil.Factory) *DeviceGroupFetcher { return &DeviceGroupFetcher{ - client: client, + CumulocityFetcher: &CumulocityFetcher{ + factory: factory, + }, } } func (f *DeviceGroupFetcher) getByID(id string) ([]fetcherResultSet, error) { - mo, resp, err := f.client.Inventory.GetManagedObject( - WithDisabledDryRunContext(f.client), + mo, resp, err := f.Client().Inventory.GetManagedObject( + WithDisabledDryRunContext(f.Client()), id, nil, ) @@ -39,8 +41,8 @@ func (f *DeviceGroupFetcher) getByID(id string) ([]fetcherResultSet, error) { } func (f *DeviceGroupFetcher) getByName(name string) ([]fetcherResultSet, error) { - mcol, _, err := f.client.Inventory.GetManagedObjects( - WithDisabledDryRunContext(f.client), + mcol, _, err := f.Client().Inventory.GetManagedObjects( + WithDisabledDryRunContext(f.Client()), &c8y.ManagedObjectOptions{ Query: fmt.Sprintf("has(c8y_IsDeviceGroup) and name eq '%s'", name), PaginationOptions: *c8y.NewPaginationOptions(5), diff --git a/pkg/c8yfetcher/deviceProfileFetcher.go b/pkg/c8yfetcher/deviceProfileFetcher.go index 740577bc5..4fbd36ec0 100644 --- a/pkg/c8yfetcher/deviceProfileFetcher.go +++ b/pkg/c8yfetcher/deviceProfileFetcher.go @@ -4,23 +4,25 @@ import ( "fmt" "github.com/pkg/errors" + "github.com/reubenmiller/go-c8y-cli/v2/pkg/cmdutil" "github.com/reubenmiller/go-c8y/pkg/c8y" ) type DeviceProfileFetcher struct { - client *c8y.Client - *DefaultFetcher + *CumulocityFetcher } -func NewDeviceProfileFetcher(client *c8y.Client) *DeviceProfileFetcher { +func NewDeviceProfileFetcher(factory *cmdutil.Factory) *DeviceProfileFetcher { return &DeviceProfileFetcher{ - client: client, + CumulocityFetcher: &CumulocityFetcher{ + factory: factory, + }, } } func (f *DeviceProfileFetcher) getByID(id string) ([]fetcherResultSet, error) { - mo, resp, err := f.client.Inventory.GetManagedObject( - WithDisabledDryRunContext(f.client), + mo, resp, err := f.Client().Inventory.GetManagedObject( + WithDisabledDryRunContext(f.Client()), id, nil, ) @@ -39,8 +41,8 @@ func (f *DeviceProfileFetcher) getByID(id string) ([]fetcherResultSet, error) { } func (f *DeviceProfileFetcher) getByName(name string) ([]fetcherResultSet, error) { - mcol, _, err := f.client.Inventory.GetManagedObjects( - WithDisabledDryRunContext(f.client), + mcol, _, err := f.Client().Inventory.GetManagedObjects( + WithDisabledDryRunContext(f.Client()), &c8y.ManagedObjectOptions{ Query: fmt.Sprintf("(type eq 'c8y_Profile') and name eq '%s'", name), PaginationOptions: *c8y.NewPaginationOptions(5), diff --git a/pkg/c8yfetcher/deviceServiceFetcher.go b/pkg/c8yfetcher/deviceServiceFetcher.go index 196d4807c..5cf42ea57 100644 --- a/pkg/c8yfetcher/deviceServiceFetcher.go +++ b/pkg/c8yfetcher/deviceServiceFetcher.go @@ -3,6 +3,7 @@ package c8yfetcher import ( "fmt" + "github.com/reubenmiller/go-c8y-cli/v2/pkg/cmdutil" "github.com/reubenmiller/go-c8y/pkg/c8y" ) @@ -10,11 +11,19 @@ type DeviceServiceFetcher struct { *ManagedObjectFetcher } -func NewDeviceServiceFetcher(client *c8y.Client, device string) *DeviceServiceFetcher { +func NewDeviceServiceFetcher(factory *cmdutil.Factory, device string) *DeviceServiceFetcher { return &DeviceServiceFetcher{ ManagedObjectFetcher: &ManagedObjectFetcher{ - client: client, + CumulocityFetcher: &CumulocityFetcher{ + factory: factory, + }, Query: func(s string) string { + + client, err := factory.Client() + if err != nil { + return "" + } + if !IsID(device) { // Lookup software by name moDevice, _, err := client.Inventory.GetDevicesByName(WithDisabledDryRunContext(client), device, &c8y.PaginationOptions{ diff --git a/pkg/c8yfetcher/externalFetcher.go b/pkg/c8yfetcher/externalFetcher.go new file mode 100644 index 000000000..8b4d004c5 --- /dev/null +++ b/pkg/c8yfetcher/externalFetcher.go @@ -0,0 +1,62 @@ +package c8yfetcher + +import ( + "bytes" + "regexp" + + "github.com/pkg/errors" + "github.com/reubenmiller/go-c8y-cli/v2/pkg/extexec" +) + +type ExternalFetcher struct { + externalCommand []string + IDPattern *regexp.Regexp + *DefaultFetcher +} + +func NewExternalFetcher(externalCommand []string, idPattern string) *ExternalFetcher { + opt := ExternalFetcher{ + externalCommand: externalCommand, + } + + if idPattern != "" { + if p, err := regexp.Compile(idPattern); err == nil { + opt.IDPattern = p + } + } + return &opt +} + +func (f *ExternalFetcher) IsID(v string) bool { + if f.IDPattern != nil { + return f.IDPattern.MatchString(v) + } + return false +} + +func (f *ExternalFetcher) getByID(id string) ([]fetcherResultSet, error) { + results := make([]fetcherResultSet, 1) + results[0] = fetcherResultSet{ + ID: id, + } + return results, nil +} + +func (f *ExternalFetcher) getByName(name string) ([]fetcherResultSet, error) { + output, err := extexec.ExecuteExternalCommand(name, f.externalCommand) + + if err != nil { + return nil, errors.Wrap(err, "Could not fetch by name") + } + + results := make([]fetcherResultSet, 0) + for _, row := range bytes.Split(output, []byte("\n")) { + if len(row) > 0 { + results = append(results, fetcherResultSet{ + ID: string(row), + }) + } + } + + return results, nil +} diff --git a/pkg/c8yfetcher/firmwareFetcher.go b/pkg/c8yfetcher/firmwareFetcher.go index d3fd5ddd5..932615ea5 100644 --- a/pkg/c8yfetcher/firmwareFetcher.go +++ b/pkg/c8yfetcher/firmwareFetcher.go @@ -3,17 +3,19 @@ package c8yfetcher import ( "fmt" - "github.com/reubenmiller/go-c8y/pkg/c8y" + "github.com/reubenmiller/go-c8y-cli/v2/pkg/cmdutil" ) type FirmwareFetcher struct { *ManagedObjectFetcher } -func NewFirmwareFetcher(client *c8y.Client) *FirmwareFetcher { +func NewFirmwareFetcher(factory *cmdutil.Factory) *FirmwareFetcher { return &FirmwareFetcher{ ManagedObjectFetcher: &ManagedObjectFetcher{ - client: client, + CumulocityFetcher: &CumulocityFetcher{ + factory: factory, + }, Query: func(s string) string { return fmt.Sprintf("(type eq 'c8y_Firmware') and name eq '%s'", s) }, diff --git a/pkg/c8yfetcher/firmwareVersionFetcher.go b/pkg/c8yfetcher/firmwareVersionFetcher.go index 409e98af4..507b2a80a 100644 --- a/pkg/c8yfetcher/firmwareVersionFetcher.go +++ b/pkg/c8yfetcher/firmwareVersionFetcher.go @@ -1,9 +1,9 @@ package c8yfetcher import ( - "context" "fmt" + "github.com/reubenmiller/go-c8y-cli/v2/pkg/cmdutil" "github.com/reubenmiller/go-c8y/pkg/c8y" ) @@ -11,17 +11,23 @@ type FirmwareVersionFetcher struct { *ManagedObjectFetcher } -func NewFirmwareVersionFetcher(client *c8y.Client, firmware string, includePatch bool) *FirmwareVersionFetcher { +func NewFirmwareVersionFetcher(factory *cmdutil.Factory, firmware string, includePatch bool) *FirmwareVersionFetcher { return &FirmwareVersionFetcher{ ManagedObjectFetcher: &ManagedObjectFetcher{ - client: client, + CumulocityFetcher: &CumulocityFetcher{ + factory: factory, + }, Query: func(s string) string { + client, err := factory.Client() + if err != nil { + return "" + } var firmwareID string if IsID(firmware) { firmwareID = firmware } else { // Lookup firmware by name - res, _, err := client.Firmware.GetFirmwareByName(context.Background(), firmware, c8y.NewPaginationOptions(5)) + res, _, err := client.Firmware.GetFirmwareByName(WithDisabledDryRunContext(client), firmware, c8y.NewPaginationOptions(5)) if err == nil && len(res.ManagedObjects) > 0 { firmwareID = res.ManagedObjects[0].ID } diff --git a/pkg/c8yfetcher/hostedApplicationFetcher.go b/pkg/c8yfetcher/hostedApplicationFetcher.go index 0cae822cd..dacdf3934 100644 --- a/pkg/c8yfetcher/hostedApplicationFetcher.go +++ b/pkg/c8yfetcher/hostedApplicationFetcher.go @@ -5,23 +5,27 @@ import ( "regexp" "github.com/pkg/errors" + "github.com/reubenmiller/go-c8y-cli/v2/pkg/cmdutil" "github.com/reubenmiller/go-c8y/pkg/c8y" ) type HostedApplicationFetcher struct { - client *c8y.Client - *DefaultFetcher + excludeParentTenant bool + *CumulocityFetcher } -func NewHostedApplicationFetcher(client *c8y.Client) *HostedApplicationFetcher { +func NewHostedApplicationFetcher(factory *cmdutil.Factory, excludeParentTenant bool) *HostedApplicationFetcher { return &HostedApplicationFetcher{ - client: client, + excludeParentTenant: excludeParentTenant, + CumulocityFetcher: &CumulocityFetcher{ + factory: factory, + }, } } func (f *HostedApplicationFetcher) getByID(id string) ([]fetcherResultSet, error) { - app, resp, err := f.client.Application.GetApplication( - WithDisabledDryRunContext(f.client), + app, resp, err := f.Client().Application.GetApplication( + WithDisabledDryRunContext(f.Client()), id, ) @@ -40,8 +44,8 @@ func (f *HostedApplicationFetcher) getByID(id string) ([]fetcherResultSet, error // getByName returns applications matching a given using regular expression func (f *HostedApplicationFetcher) getByName(name string) ([]fetcherResultSet, error) { - col, _, err := f.client.Application.GetApplications( - WithDisabledDryRunContext(f.client), + col, _, err := f.Client().Application.GetApplications( + WithDisabledDryRunContext(f.Client()), &c8y.ApplicationOptions{ PaginationOptions: *c8y.NewPaginationOptions(2000), @@ -64,15 +68,16 @@ func (f *HostedApplicationFetcher) getByName(name string) ([]fetcherResultSet, e results := make([]fetcherResultSet, 0) + // First check for hosted applications owned by the current tenant for i, app := range col.Applications { if app.Type == "HOSTED" && pattern.MatchString(app.Name) { // Ignore applications which don't match the owner // so that it can overwrite existing applications such as cockpit and devicemanagement. // Otherwise it will always match the in-built apps - if f.client.TenantName != "" { + if f.Client().TenantName != "" { if app.Owner != nil && app.Owner.Tenant != nil && app.Owner.Tenant.ID != "" { - if app.Owner.Tenant.ID != f.client.TenantName { + if app.Owner.Tenant.ID != f.Client().TenantName { continue } } @@ -84,7 +89,19 @@ func (f *HostedApplicationFetcher) getByName(name string) ([]fetcherResultSet, e Value: col.Items[i], }) } + } + // If not results are found, then also include any matches (not just in the current tenant) + if len(results) == 0 && !f.excludeParentTenant { + for i, app := range col.Applications { + if app.Type == "HOSTED" && pattern.MatchString(app.Name) { + results = append(results, fetcherResultSet{ + ID: app.ID, + Name: app.Name, + Value: col.Items[i], + }) + } + } } return results, nil @@ -93,8 +110,8 @@ func (f *HostedApplicationFetcher) getByName(name string) ([]fetcherResultSet, e // FindHostedApplications returns hosted applications given either an id or search text // @values: An array of ids, or names (with wildcards) // @lookupID: Lookup the data if an id is given. If a non-id text is given, the result will always be looked up. -func FindHostedApplications(client *c8y.Client, values []string, lookupID bool, format string) ([]entityReference, error) { - f := NewHostedApplicationFetcher(client) +func FindHostedApplications(factory *cmdutil.Factory, values []string, lookupID bool, format string, excludeParentTenant bool) ([]entityReference, error) { + f := NewHostedApplicationFetcher(factory, excludeParentTenant) formattedValues, err := lookupEntity(f, values, lookupID, format) diff --git a/pkg/c8yfetcher/managedObjectFetcher.go b/pkg/c8yfetcher/managedObjectFetcher.go index afdc659fe..50a4b5608 100644 --- a/pkg/c8yfetcher/managedObjectFetcher.go +++ b/pkg/c8yfetcher/managedObjectFetcher.go @@ -2,26 +2,28 @@ package c8yfetcher import ( "github.com/pkg/errors" + "github.com/reubenmiller/go-c8y-cli/v2/pkg/cmdutil" "github.com/reubenmiller/go-c8y/pkg/c8y" ) type QueryFilter func(string) string type ManagedObjectFetcher struct { - client *c8y.Client - Query QueryFilter - *DefaultFetcher + Query QueryFilter + *CumulocityFetcher } -func NewManagedObjectFetcher(client *c8y.Client) *ManagedObjectFetcher { +func NewManagedObjectFetcher(factory *cmdutil.Factory) *ManagedObjectFetcher { return &ManagedObjectFetcher{ - client: client, + CumulocityFetcher: &CumulocityFetcher{ + factory: factory, + }, } } func (f *ManagedObjectFetcher) getByID(id string) ([]fetcherResultSet, error) { - mo, resp, err := f.client.Inventory.GetManagedObject( - WithDisabledDryRunContext(f.client), + mo, resp, err := f.Client().Inventory.GetManagedObject( + WithDisabledDryRunContext(f.Client()), id, nil, ) @@ -44,8 +46,8 @@ func (f *ManagedObjectFetcher) getByName(name string) ([]fetcherResultSet, error if f.Query != nil { query = f.Query(name) } - mcol, _, err := f.client.Inventory.GetManagedObjects( - WithDisabledDryRunContext(f.client), + mcol, _, err := f.Client().Inventory.GetManagedObjects( + WithDisabledDryRunContext(f.Client()), &c8y.ManagedObjectOptions{ Query: query, PaginationOptions: *c8y.NewPaginationOptions(5), diff --git a/pkg/c8yfetcher/microserviceFetcher.go b/pkg/c8yfetcher/microserviceFetcher.go index 417282789..ce503f882 100644 --- a/pkg/c8yfetcher/microserviceFetcher.go +++ b/pkg/c8yfetcher/microserviceFetcher.go @@ -5,23 +5,25 @@ import ( "regexp" "github.com/pkg/errors" + "github.com/reubenmiller/go-c8y-cli/v2/pkg/cmdutil" "github.com/reubenmiller/go-c8y/pkg/c8y" ) type MicroserviceFetcher struct { - client *c8y.Client - *DefaultFetcher + *CumulocityFetcher } -func NewMicroserviceFetcher(client *c8y.Client) *MicroserviceFetcher { +func NewMicroserviceFetcher(factory *cmdutil.Factory) *MicroserviceFetcher { return &MicroserviceFetcher{ - client: client, + CumulocityFetcher: &CumulocityFetcher{ + factory: factory, + }, } } func (f *MicroserviceFetcher) getByID(id string) ([]fetcherResultSet, error) { - app, resp, err := f.client.Application.GetApplication( - WithDisabledDryRunContext(f.client), + app, resp, err := f.Client().Application.GetApplication( + WithDisabledDryRunContext(f.Client()), id, ) @@ -40,8 +42,8 @@ func (f *MicroserviceFetcher) getByID(id string) ([]fetcherResultSet, error) { // getByName returns applications matching a given using regular expression func (f *MicroserviceFetcher) getByName(name string) ([]fetcherResultSet, error) { - col, _, err := f.client.Application.GetApplications( - WithDisabledDryRunContext(f.client), + col, _, err := f.Client().Application.GetApplications( + WithDisabledDryRunContext(f.Client()), &c8y.ApplicationOptions{ PaginationOptions: *c8y.NewPaginationOptions(2000), @@ -81,8 +83,8 @@ func (f *MicroserviceFetcher) getByName(name string) ([]fetcherResultSet, error) // findMicroservices returns microservices given either an id or search text // @values: An array of ids, or names (with wildcards) // @lookupID: Lookup the data if an id is given. If a non-id text is given, the result will always be looked up. -func FindMicroservices(client *c8y.Client, values []string, lookupID bool, format string) ([]entityReference, error) { - f := NewMicroserviceFetcher(client) +func FindMicroservices(factory *cmdutil.Factory, values []string, lookupID bool, format string) ([]entityReference, error) { + f := NewMicroserviceFetcher(factory) formattedValues, err := lookupEntity(f, values, lookupID, format) diff --git a/pkg/c8yfetcher/roleFetcher.go b/pkg/c8yfetcher/roleFetcher.go index cfb6add31..b0058aa0f 100644 --- a/pkg/c8yfetcher/roleFetcher.go +++ b/pkg/c8yfetcher/roleFetcher.go @@ -4,18 +4,21 @@ import ( "strings" "github.com/pkg/errors" + "github.com/reubenmiller/go-c8y-cli/v2/pkg/cmdutil" "github.com/reubenmiller/go-c8y-cli/v2/pkg/matcher" "github.com/reubenmiller/go-c8y/pkg/c8y" ) type RoleFetcher struct { - client *c8y.Client + *CumulocityFetcher *IDNameFetcher } -func NewRoleFetcher(client *c8y.Client) *RoleFetcher { +func NewRoleFetcher(factory *cmdutil.Factory) *RoleFetcher { return &RoleFetcher{ - client: client, + CumulocityFetcher: &CumulocityFetcher{ + factory: factory, + }, } } @@ -31,8 +34,8 @@ func (f *RoleFetcher) getByID(id string) ([]fetcherResultSet, error) { }, nil } - role, resp, err := f.client.User.GetRole( - WithDisabledDryRunContext(f.client), + role, resp, err := f.Client().User.GetRole( + WithDisabledDryRunContext(f.Client()), id, ) @@ -62,8 +65,8 @@ func (f *RoleFetcher) getByName(name string) ([]fetcherResultSet, error) { }, }, nil } - roles, _, err := f.client.User.GetRoles( - WithDisabledDryRunContext(f.client), + roles, _, err := f.Client().User.GetRoles( + WithDisabledDryRunContext(f.Client()), &c8y.RoleOptions{ PaginationOptions: *c8y.NewPaginationOptions(100), }, diff --git a/pkg/c8yfetcher/smartGroupFetcher.manual.go b/pkg/c8yfetcher/smartGroupFetcher.manual.go index d48bf9e05..b8e839ff1 100644 --- a/pkg/c8yfetcher/smartGroupFetcher.manual.go +++ b/pkg/c8yfetcher/smartGroupFetcher.manual.go @@ -4,23 +4,25 @@ import ( "fmt" "github.com/pkg/errors" + "github.com/reubenmiller/go-c8y-cli/v2/pkg/cmdutil" "github.com/reubenmiller/go-c8y/pkg/c8y" ) type SmartGroupFetcher struct { - client *c8y.Client - *DefaultFetcher + *CumulocityFetcher } -func NewSmartGroupFetcher(client *c8y.Client) *SmartGroupFetcher { +func NewSmartGroupFetcher(factory *cmdutil.Factory) *SmartGroupFetcher { return &SmartGroupFetcher{ - client: client, + CumulocityFetcher: &CumulocityFetcher{ + factory: factory, + }, } } func (f *SmartGroupFetcher) getByID(id string) ([]fetcherResultSet, error) { - mo, resp, err := f.client.Inventory.GetManagedObject( - WithDisabledDryRunContext(f.client), + mo, resp, err := f.Client().Inventory.GetManagedObject( + WithDisabledDryRunContext(f.Client()), id, nil, ) @@ -39,8 +41,8 @@ func (f *SmartGroupFetcher) getByID(id string) ([]fetcherResultSet, error) { } func (f *SmartGroupFetcher) getByName(name string) ([]fetcherResultSet, error) { - mcol, _, err := f.client.Inventory.GetManagedObjects( - WithDisabledDryRunContext(f.client), + mcol, _, err := f.Client().Inventory.GetManagedObjects( + WithDisabledDryRunContext(f.Client()), &c8y.ManagedObjectOptions{ Query: fmt.Sprintf("type eq 'c8y_DynamicGroup' and name eq '%s'", name), PaginationOptions: *c8y.NewPaginationOptions(5), diff --git a/pkg/c8yfetcher/softwareFetcher.go b/pkg/c8yfetcher/softwareFetcher.go index 6d1d49d72..75fd8b2c4 100644 --- a/pkg/c8yfetcher/softwareFetcher.go +++ b/pkg/c8yfetcher/softwareFetcher.go @@ -3,17 +3,19 @@ package c8yfetcher import ( "fmt" - "github.com/reubenmiller/go-c8y/pkg/c8y" + "github.com/reubenmiller/go-c8y-cli/v2/pkg/cmdutil" ) type SoftwareFetcher struct { *ManagedObjectFetcher } -func NewSoftwareFetcher(client *c8y.Client) *SoftwareFetcher { +func NewSoftwareFetcher(factory *cmdutil.Factory) *SoftwareFetcher { return &SoftwareFetcher{ ManagedObjectFetcher: &ManagedObjectFetcher{ - client: client, + CumulocityFetcher: &CumulocityFetcher{ + factory: factory, + }, Query: func(s string) string { return fmt.Sprintf("(type eq 'c8y_Software') and name eq '%s'", s) }, diff --git a/pkg/c8yfetcher/softwareVersionFetcher.go b/pkg/c8yfetcher/softwareVersionFetcher.go index 6d0198ab7..53bb5237e 100644 --- a/pkg/c8yfetcher/softwareVersionFetcher.go +++ b/pkg/c8yfetcher/softwareVersionFetcher.go @@ -3,6 +3,7 @@ package c8yfetcher import ( "fmt" + "github.com/reubenmiller/go-c8y-cli/v2/pkg/cmdutil" "github.com/reubenmiller/go-c8y/pkg/c8y" ) @@ -10,12 +11,18 @@ type SoftwareVersionFetcher struct { *ManagedObjectFetcher } -func NewSoftwareVersionFetcher(client *c8y.Client, software string) *SoftwareVersionFetcher { +func NewSoftwareVersionFetcher(factory *cmdutil.Factory, software string) *SoftwareVersionFetcher { return &SoftwareVersionFetcher{ ManagedObjectFetcher: &ManagedObjectFetcher{ - client: client, + CumulocityFetcher: &CumulocityFetcher{ + factory: factory, + }, Query: func(s string) string { // Check + client, err := factory.Client() + if err != nil { + return "" + } if !IsID(software) { // Lookup software by name diff --git a/pkg/c8yfetcher/userFetcher.go b/pkg/c8yfetcher/userFetcher.go index bb9a332dc..061883e37 100644 --- a/pkg/c8yfetcher/userFetcher.go +++ b/pkg/c8yfetcher/userFetcher.go @@ -4,24 +4,27 @@ import ( "strings" "github.com/pkg/errors" + "github.com/reubenmiller/go-c8y-cli/v2/pkg/cmdutil" "github.com/reubenmiller/go-c8y-cli/v2/pkg/matcher" "github.com/reubenmiller/go-c8y/pkg/c8y" ) type UserFetcher struct { - client *c8y.Client + *CumulocityFetcher *IDNameFetcher } -func NewUserFetcher(client *c8y.Client) *UserFetcher { +func NewUserFetcher(factory *cmdutil.Factory) *UserFetcher { return &UserFetcher{ - client: client, + CumulocityFetcher: &CumulocityFetcher{ + factory: factory, + }, } } func (f *UserFetcher) getByID(id string) ([]fetcherResultSet, error) { - user, resp, err := f.client.User.GetUser( - WithDisabledDryRunContext(f.client), + user, resp, err := f.Client().User.GetUser( + WithDisabledDryRunContext(f.Client()), id, ) @@ -39,8 +42,8 @@ func (f *UserFetcher) getByID(id string) ([]fetcherResultSet, error) { } func (f *UserFetcher) getByName(name string) ([]fetcherResultSet, error) { - users, _, err := f.client.User.GetUsers( - WithDisabledDryRunContext(f.client), + users, _, err := f.Client().User.GetUsers( + WithDisabledDryRunContext(f.Client()), &c8y.UserOptions{ Username: strings.ReplaceAll(name, "*", ""), PaginationOptions: *c8y.NewPaginationOptions(5), diff --git a/pkg/c8yfetcher/userGroupFetcher.go b/pkg/c8yfetcher/userGroupFetcher.go index 34734f34d..d04eb335c 100644 --- a/pkg/c8yfetcher/userGroupFetcher.go +++ b/pkg/c8yfetcher/userGroupFetcher.go @@ -2,24 +2,26 @@ package c8yfetcher import ( "github.com/pkg/errors" + "github.com/reubenmiller/go-c8y-cli/v2/pkg/cmdutil" "github.com/reubenmiller/go-c8y-cli/v2/pkg/matcher" "github.com/reubenmiller/go-c8y/pkg/c8y" ) type UserGroupFetcher struct { - client *c8y.Client - *DefaultFetcher + *CumulocityFetcher } -func NewUserGroupFetcher(client *c8y.Client) *UserGroupFetcher { +func NewUserGroupFetcher(factory *cmdutil.Factory) *UserGroupFetcher { return &UserGroupFetcher{ - client: client, + CumulocityFetcher: &CumulocityFetcher{ + factory: factory, + }, } } func (f *UserGroupFetcher) getByID(id string) ([]fetcherResultSet, error) { - group, resp, err := f.client.User.GetGroup( - WithDisabledDryRunContext(f.client), + group, resp, err := f.Client().User.GetGroup( + WithDisabledDryRunContext(f.Client()), id, ) @@ -37,8 +39,8 @@ func (f *UserGroupFetcher) getByID(id string) ([]fetcherResultSet, error) { } func (f *UserGroupFetcher) getByName(name string) ([]fetcherResultSet, error) { - groups, _, err := f.client.User.GetGroups( - WithDisabledDryRunContext(f.client), + groups, _, err := f.Client().User.GetGroups( + WithDisabledDryRunContext(f.Client()), &c8y.GroupOptions{ PaginationOptions: *c8y.NewPaginationOptions(2000), }, diff --git a/pkg/c8yquery/query.go b/pkg/c8yquery/query.go index fd5636b35..0b64da256 100644 --- a/pkg/c8yquery/query.go +++ b/pkg/c8yquery/query.go @@ -65,7 +65,9 @@ func (i *CumulocityQueryIterator) GetNext() (line []byte, input interface{}, err queryParts = append(queryParts, UnescapeValue(format, string(line))) } default: - queryParts = append(queryParts, UnescapeValue(format, fmt.Sprintf("%v", value))) + if v := fmt.Sprintf("%v", value); v != "" { + queryParts = append(queryParts, UnescapeValue(format, v)) + } } } diff --git a/pkg/cmd/agents/create/create.auto.go b/pkg/cmd/agents/create/create.auto.go index 6f6c12fb4..92d171cff 100644 --- a/pkg/cmd/agents/create/create.auto.go +++ b/pkg/cmd/agents/create/create.auto.go @@ -145,7 +145,7 @@ func (n *CreateCmd) RunE(cmd *cobra.Command, args []string) error { flags.WithStringValue("type", "type"), flags.WithRequiredTemplateString(` {c8y_IsDevice: {}, com_cumulocity_model_Agent: {}}`), - cmdutil.WithTemplateValue(cfg), + cmdutil.WithTemplateValue(n.factory), flags.WithTemplateVariablesValue(), flags.WithRequiredProperties("name"), ) diff --git a/pkg/cmd/agents/delete/delete.auto.go b/pkg/cmd/agents/delete/delete.auto.go index 4e41829e5..09ab7409a 100644 --- a/pkg/cmd/agents/delete/delete.auto.go +++ b/pkg/cmd/agents/delete/delete.auto.go @@ -158,7 +158,7 @@ func (n *DeleteCmd) RunE(cmd *cobra.Command, args []string) error { cmd, path, inputIterators, - c8yfetcher.WithAgentByNameFirstMatch(client, args, "id", "id"), + c8yfetcher.WithAgentByNameFirstMatch(n.factory, args, "id", "id"), ) if err != nil { return err diff --git a/pkg/cmd/agents/get/get.auto.go b/pkg/cmd/agents/get/get.auto.go index bdf174575..ef17f01f5 100644 --- a/pkg/cmd/agents/get/get.auto.go +++ b/pkg/cmd/agents/get/get.auto.go @@ -160,7 +160,7 @@ func (n *GetCmd) RunE(cmd *cobra.Command, args []string) error { cmd, path, inputIterators, - c8yfetcher.WithAgentByNameFirstMatch(client, args, "id", "id"), + c8yfetcher.WithAgentByNameFirstMatch(n.factory, args, "id", "id"), ) if err != nil { return err diff --git a/pkg/cmd/agents/list/list.auto.go b/pkg/cmd/agents/list/list.auto.go index 6308e2bb4..0b54702dd 100644 --- a/pkg/cmd/agents/list/list.auto.go +++ b/pkg/cmd/agents/list/list.auto.go @@ -149,7 +149,7 @@ func (n *ListCmd) RunE(cmd *cobra.Command, args []string) error { flags.WithEncodedRelativeTimestamp("lastMessageDateFrom", "lastMessageDateFrom", "(c8y_Availability.lastMessage ge '%s')"), flags.WithEncodedRelativeTimestamp("creationTimeDateTo", "creationTimeDateTo", "(creationTime.date le '%s')"), flags.WithEncodedRelativeTimestamp("creationTimeDateFrom", "creationTimeDateFrom", "(creationTime.date ge '%s')"), - c8yfetcher.WithDeviceGroupByNameFirstMatch(client, args, "group", "group", "bygroupid(%s)"), + c8yfetcher.WithDeviceGroupByNameFirstMatch(n.factory, args, "group", "group", "bygroupid(%s)"), }, "q", ), diff --git a/pkg/cmd/agents/update/update.auto.go b/pkg/cmd/agents/update/update.auto.go index 7df5759d6..39365f474 100644 --- a/pkg/cmd/agents/update/update.auto.go +++ b/pkg/cmd/agents/update/update.auto.go @@ -139,7 +139,7 @@ func (n *UpdateCmd) RunE(cmd *cobra.Command, args []string) error { inputIterators, flags.WithDataFlagValue(), flags.WithStringValue("newName", "name"), - cmdutil.WithTemplateValue(cfg), + cmdutil.WithTemplateValue(n.factory), flags.WithTemplateVariablesValue(), ) if err != nil { @@ -152,7 +152,7 @@ func (n *UpdateCmd) RunE(cmd *cobra.Command, args []string) error { cmd, path, inputIterators, - c8yfetcher.WithAgentByNameFirstMatch(client, args, "id", "id"), + c8yfetcher.WithAgentByNameFirstMatch(n.factory, args, "id", "id"), ) if err != nil { return err diff --git a/pkg/cmd/alarms/count/count.auto.go b/pkg/cmd/alarms/count/count.auto.go index c474fdfa0..66b527e23 100644 --- a/pkg/cmd/alarms/count/count.auto.go +++ b/pkg/cmd/alarms/count/count.auto.go @@ -115,7 +115,7 @@ func (n *CountCmd) RunE(cmd *cobra.Command, args []string) error { query, inputIterators, flags.WithCustomStringSlice(func() ([]string, error) { return cfg.GetQueryParameters(), nil }, "custom"), - c8yfetcher.WithDeviceByNameFirstMatch(client, args, "device", "source"), + c8yfetcher.WithDeviceByNameFirstMatch(n.factory, args, "device", "source"), flags.WithEncodedRelativeTimestamp("dateFrom", "dateFrom"), flags.WithEncodedRelativeTimestamp("dateTo", "dateTo"), flags.WithStringValue("type", "type"), diff --git a/pkg/cmd/alarms/create/create.auto.go b/pkg/cmd/alarms/create/create.auto.go index ffe361c8a..e5cb9d71f 100644 --- a/pkg/cmd/alarms/create/create.auto.go +++ b/pkg/cmd/alarms/create/create.auto.go @@ -145,7 +145,7 @@ func (n *CreateCmd) RunE(cmd *cobra.Command, args []string) error { body, inputIterators, flags.WithDataFlagValue(), - c8yfetcher.WithDeviceByNameFirstMatch(client, args, "device", "source.id"), + c8yfetcher.WithDeviceByNameFirstMatch(n.factory, args, "device", "source.id"), flags.WithStringValue("type", "type"), flags.WithRelativeTimestamp("time", "time"), flags.WithStringValue("text", "text"), @@ -153,7 +153,7 @@ func (n *CreateCmd) RunE(cmd *cobra.Command, args []string) error { flags.WithStringValue("status", "status"), flags.WithDefaultTemplateString(` {time: _.Now('0s')}`), - cmdutil.WithTemplateValue(cfg), + cmdutil.WithTemplateValue(n.factory), flags.WithTemplateVariablesValue(), flags.WithRequiredProperties("type", "text", "time", "severity", "source.id"), ) diff --git a/pkg/cmd/alarms/deletecollection/deleteCollection.auto.go b/pkg/cmd/alarms/deletecollection/deleteCollection.auto.go index 84a185f12..dacecae3f 100644 --- a/pkg/cmd/alarms/deletecollection/deleteCollection.auto.go +++ b/pkg/cmd/alarms/deletecollection/deleteCollection.auto.go @@ -113,7 +113,7 @@ func (n *DeleteCollectionCmd) RunE(cmd *cobra.Command, args []string) error { query, inputIterators, flags.WithCustomStringSlice(func() ([]string, error) { return cfg.GetQueryParameters(), nil }, "custom"), - c8yfetcher.WithDeviceByNameFirstMatch(client, args, "device", "source"), + c8yfetcher.WithDeviceByNameFirstMatch(n.factory, args, "device", "source"), flags.WithEncodedRelativeTimestamp("dateFrom", "dateFrom"), flags.WithEncodedRelativeTimestamp("dateTo", "dateTo"), flags.WithEncodedRelativeTimestamp("createdFrom", "createdFrom"), diff --git a/pkg/cmd/alarms/list/list.auto.go b/pkg/cmd/alarms/list/list.auto.go index e23bf9c80..a33ba3c7c 100644 --- a/pkg/cmd/alarms/list/list.auto.go +++ b/pkg/cmd/alarms/list/list.auto.go @@ -122,7 +122,7 @@ func (n *ListCmd) RunE(cmd *cobra.Command, args []string) error { query, inputIterators, flags.WithCustomStringSlice(func() ([]string, error) { return cfg.GetQueryParameters(), nil }, "custom"), - c8yfetcher.WithDeviceByNameFirstMatch(client, args, "device", "source"), + c8yfetcher.WithDeviceByNameFirstMatch(n.factory, args, "device", "source"), flags.WithEncodedRelativeTimestamp("dateFrom", "dateFrom"), flags.WithEncodedRelativeTimestamp("dateTo", "dateTo"), flags.WithStringValue("type", "type"), diff --git a/pkg/cmd/alarms/subscribe/subscribe.manual.go b/pkg/cmd/alarms/subscribe/subscribe.manual.go index 0924d56df..cff7a1084 100644 --- a/pkg/cmd/alarms/subscribe/subscribe.manual.go +++ b/pkg/cmd/alarms/subscribe/subscribe.manual.go @@ -95,7 +95,7 @@ func (n *CmdSubscribe) RunE(cmd *cobra.Command, args []string) error { path, inputIterators, flags.WithStringDefaultValue("*", "device", "device"), - c8yfetcher.WithDeviceByNameFirstMatch(client, args, "device", "device"), + c8yfetcher.WithDeviceByNameFirstMatch(n.factory, args, "device", "device"), ) if err != nil { return err diff --git a/pkg/cmd/alarms/update/update.auto.go b/pkg/cmd/alarms/update/update.auto.go index 759fcf3e1..21cc07aab 100644 --- a/pkg/cmd/alarms/update/update.auto.go +++ b/pkg/cmd/alarms/update/update.auto.go @@ -146,7 +146,7 @@ func (n *UpdateCmd) RunE(cmd *cobra.Command, args []string) error { flags.WithStringValue("status", "status"), flags.WithStringValue("severity", "severity"), flags.WithStringValue("text", "text"), - cmdutil.WithTemplateValue(cfg), + cmdutil.WithTemplateValue(n.factory), flags.WithTemplateVariablesValue(), ) if err != nil { diff --git a/pkg/cmd/alarms/updatecollection/updateCollection.auto.go b/pkg/cmd/alarms/updatecollection/updateCollection.auto.go index 9033039a1..84f4c1217 100644 --- a/pkg/cmd/alarms/updatecollection/updateCollection.auto.go +++ b/pkg/cmd/alarms/updatecollection/updateCollection.auto.go @@ -110,7 +110,7 @@ func (n *UpdateCollectionCmd) RunE(cmd *cobra.Command, args []string) error { query, inputIterators, flags.WithCustomStringSlice(func() ([]string, error) { return cfg.GetQueryParameters(), nil }, "custom"), - c8yfetcher.WithDeviceByNameFirstMatch(client, args, "device", "source"), + c8yfetcher.WithDeviceByNameFirstMatch(n.factory, args, "device", "source"), flags.WithStringValue("status", "status"), flags.WithStringValue("severity", "severity"), flags.WithBoolValue("resolved", "resolved", ""), @@ -161,7 +161,7 @@ func (n *UpdateCollectionCmd) RunE(cmd *cobra.Command, args []string) error { flags.WithStringValue("newStatus", "status"), flags.WithRelativeTimestamp("createdFrom", "createdFrom"), flags.WithRelativeTimestamp("createdTo", "createdTo"), - cmdutil.WithTemplateValue(cfg), + cmdutil.WithTemplateValue(n.factory), flags.WithTemplateVariablesValue(), flags.WithRequiredProperties("status"), ) diff --git a/pkg/cmd/alias/expand/expand.go b/pkg/cmd/alias/expand/expand.go index efb61a15f..a0b920a7d 100644 --- a/pkg/cmd/alias/expand/expand.go +++ b/pkg/cmd/alias/expand/expand.go @@ -15,7 +15,7 @@ import ( // ExpandAlias processes argv to see if it should be rewritten according to a user's aliases. The // second return value indicates whether the alias should be executed in a new shell process instead -// of running gh itself. +// of running c8y itself. func ExpandAlias(aliases map[string]string, args []string, findShFunc func() (string, error)) (expanded []string, isShell bool, err error) { if len(args) < 2 { // the command is lacking a subcommand diff --git a/pkg/cmd/alias/list/list.go b/pkg/cmd/alias/list/list.go index 7fd48e076..5c6c8c090 100644 --- a/pkg/cmd/alias/list/list.go +++ b/pkg/cmd/alias/list/list.go @@ -7,6 +7,7 @@ import ( "github.com/MakeNowJust/heredoc/v2" "github.com/reubenmiller/go-c8y-cli/v2/pkg/cmdutil" + "github.com/reubenmiller/go-c8y-cli/v2/pkg/extensions" "github.com/reubenmiller/go-c8y-cli/v2/pkg/iostreams" "github.com/spf13/cobra" ) @@ -26,7 +27,7 @@ func NewCmdList(f *cmdutil.Factory, runF func(*ListOptions) error) *cobra.Comman Use: "list", Short: "List your aliases", Long: heredoc.Doc(` - This command prints out all of the aliases gh is configured to use. + This command prints out all of the aliases c8y is configured to use. `), Args: cobra.NoArgs, RunE: func(cmd *cobra.Command, args []string) error { @@ -51,8 +52,14 @@ func listRun(opts *ListOptions) error { aliasCfg := cfg.Aliases() commonAliasCfg := cfg.CommonAliases() + aliasExtensions := make([]extensions.Alias, 0) + for _, ext := range opts.factory.ExtensionManager().List() { + if extAliases, err := ext.Aliases(); err == nil { + aliasExtensions = append(aliasExtensions, extAliases...) + } + } - if len(aliasCfg) == 0 && len(commonAliasCfg) == 0 { + if len(aliasCfg) == 0 && len(commonAliasCfg) == 0 && len(aliasExtensions) == 0 { if opts.IO.IsStdoutTTY() { fmt.Fprintf(opts.IO.ErrOut, "no aliases configured\n") } @@ -66,6 +73,11 @@ func listRun(opts *ListOptions) error { return err } + err = printExtensionAliases(w, opts.IO.ColorScheme(), "extension aliases", aliasExtensions) + if err != nil { + return err + } + err = printAliases(w, opts.IO.ColorScheme(), "common aliases", commonAliasCfg) if err != nil { return err @@ -95,3 +107,28 @@ func printAliases(w io.Writer, cs *iostreams.ColorScheme, title string, aliases } return nil } + +func printExtensionAliases(w io.Writer, cs *iostreams.ColorScheme, title string, aliases []extensions.Alias) error { + if len(aliases) == 0 { + return nil + } + + aliasSet := make(map[string]extensions.Alias) + keys := []string{} + for _, alias := range aliases { + keys = append(keys, alias.GetName()) + aliasSet[alias.GetName()] = alias + } + sort.Strings(keys) + + fmt.Fprintf(w, "\n%s\n", cs.Bold(cs.Magenta(title))) + + // TODO: Change to json writer + for _, alias := range keys { + _, err := fmt.Fprintf(w, "%s: %s\n", cs.CyanBold(alias), aliasSet[alias].GetCommand()) + if err != nil { + return err + } + } + return nil +} diff --git a/pkg/cmd/api/api.manual.go b/pkg/cmd/api/api.manual.go index 3c2dacbf7..b2a29d02d 100644 --- a/pkg/cmd/api/api.manual.go +++ b/pkg/cmd/api/api.manual.go @@ -286,7 +286,7 @@ func (n *CmdAPI) RunE(cmd *cobra.Command, args []string) error { body, inputIterators, flags.WithDataValueAdvanced(!n.keepProperties, !request.HasJSONHeader(&headers), flags.FlagDataName, ""), - cmdutil.WithTemplateValue(cfg), + cmdutil.WithTemplateValue(n.factory), flags.WithTemplateVariablesValue(), ) diff --git a/pkg/cmd/applications/copy/copy.auto.go b/pkg/cmd/applications/copy/copy.auto.go index e232b910e..0725043ee 100644 --- a/pkg/cmd/applications/copy/copy.auto.go +++ b/pkg/cmd/applications/copy/copy.auto.go @@ -155,7 +155,7 @@ func (n *CopyCmd) RunE(cmd *cobra.Command, args []string) error { cmd, path, inputIterators, - c8yfetcher.WithApplicationByNameFirstMatch(client, args, "id", "id"), + c8yfetcher.WithApplicationByNameFirstMatch(n.factory, args, "id", "id"), ) if err != nil { return err diff --git a/pkg/cmd/applications/create/create.auto.go b/pkg/cmd/applications/create/create.auto.go index d6eb69e37..ae4eaa420 100644 --- a/pkg/cmd/applications/create/create.auto.go +++ b/pkg/cmd/applications/create/create.auto.go @@ -154,7 +154,7 @@ func (n *CreateCmd) RunE(cmd *cobra.Command, args []string) error { flags.WithStringValue("resourcesUsername", "resourcesUsername"), flags.WithStringValue("resourcesPassword", "resourcesPassword"), flags.WithStringValue("externalUrl", "externalUrl"), - cmdutil.WithTemplateValue(cfg), + cmdutil.WithTemplateValue(n.factory), flags.WithTemplateVariablesValue(), flags.WithRequiredProperties("name", "key", "type"), ) diff --git a/pkg/cmd/applications/createbinary/createBinary.auto.go b/pkg/cmd/applications/createbinary/createBinary.auto.go index 4ae009b1e..ba2471bc5 100644 --- a/pkg/cmd/applications/createbinary/createBinary.auto.go +++ b/pkg/cmd/applications/createbinary/createBinary.auto.go @@ -134,7 +134,7 @@ func (n *CreateBinaryCmd) RunE(cmd *cobra.Command, args []string) error { cmd, formData, inputIterators, - flags.WithFormDataFileAndInfoWithTemplateSupport(cmdutil.NewTemplateResolver(cfg), "file", "data")..., + flags.WithFormDataFileAndInfoWithTemplateSupport(cmdutil.NewTemplateResolver(n.factory), "file", "data")..., ) if err != nil { return cmderrors.NewUserError(err) @@ -147,7 +147,7 @@ func (n *CreateBinaryCmd) RunE(cmd *cobra.Command, args []string) error { body, inputIterators, flags.WithDataFlagValue(), - cmdutil.WithTemplateValue(cfg), + cmdutil.WithTemplateValue(n.factory), flags.WithTemplateVariablesValue(), ) if err != nil { @@ -160,7 +160,7 @@ func (n *CreateBinaryCmd) RunE(cmd *cobra.Command, args []string) error { cmd, path, inputIterators, - c8yfetcher.WithApplicationByNameFirstMatch(client, args, "id", "id"), + c8yfetcher.WithApplicationByNameFirstMatch(n.factory, args, "id", "id"), ) if err != nil { return err diff --git a/pkg/cmd/applications/createhostedapplication/createHostedApplication.manual.go b/pkg/cmd/applications/createhostedapplication/createHostedApplication.manual.go index 83d02c8ba..0c4c9c94c 100644 --- a/pkg/cmd/applications/createhostedapplication/createHostedApplication.manual.go +++ b/pkg/cmd/applications/createhostedapplication/createHostedApplication.manual.go @@ -261,7 +261,7 @@ func (n *CmdCreateHostedApplication) RunE(cmd *cobra.Command, args []string) err // TODO: Use the default name value from n.Name rather then reading it from the args again. log.Infof("application name: %s", appDetails.Name) if appDetails.Name != "" { - refs, err := c8yfetcher.FindHostedApplications(client, []string{appDetails.Name}, true, "") + refs, err := c8yfetcher.FindHostedApplications(n.factory, []string{appDetails.Name}, true, "", true) if err != nil { return fmt.Errorf("failed to find hosted application. %s", err) @@ -284,7 +284,10 @@ func (n *CmdCreateHostedApplication) RunE(cmd *cobra.Command, args []string) err } else { // Get existing application log.Infof("Getting existing application. id=%s", applicationID) - application, response, err = client.Application.GetApplication(context.Background(), applicationID) + application, response, err = client.Application.GetApplication( + c8yfetcher.WithDisabledDryRunContext(client), + applicationID, + ) if err != nil { return fmt.Errorf("failed to get hosted application. %s", err) diff --git a/pkg/cmd/applications/delete/delete.auto.go b/pkg/cmd/applications/delete/delete.auto.go index 182748421..c072be9f0 100644 --- a/pkg/cmd/applications/delete/delete.auto.go +++ b/pkg/cmd/applications/delete/delete.auto.go @@ -157,7 +157,7 @@ func (n *DeleteCmd) RunE(cmd *cobra.Command, args []string) error { cmd, path, inputIterators, - c8yfetcher.WithApplicationByNameFirstMatch(client, args, "id", "id"), + c8yfetcher.WithApplicationByNameFirstMatch(n.factory, args, "id", "id"), ) if err != nil { return err diff --git a/pkg/cmd/applications/deleteapplicationbinary/deleteApplicationBinary.auto.go b/pkg/cmd/applications/deleteapplicationbinary/deleteApplicationBinary.auto.go index 4f97ddee9..22bada22d 100644 --- a/pkg/cmd/applications/deleteapplicationbinary/deleteApplicationBinary.auto.go +++ b/pkg/cmd/applications/deleteapplicationbinary/deleteApplicationBinary.auto.go @@ -151,7 +151,7 @@ func (n *DeleteApplicationBinaryCmd) RunE(cmd *cobra.Command, args []string) err cmd, path, inputIterators, - c8yfetcher.WithHostedApplicationByNameFirstMatch(client, args, "application", "application"), + c8yfetcher.WithHostedApplicationByNameFirstMatch(n.factory, args, "application", "application"), c8yfetcher.WithIDSlice(args, "binaryId", "binaryId"), ) if err != nil { diff --git a/pkg/cmd/applications/get/get.auto.go b/pkg/cmd/applications/get/get.auto.go index 442944325..9700c4e06 100644 --- a/pkg/cmd/applications/get/get.auto.go +++ b/pkg/cmd/applications/get/get.auto.go @@ -150,7 +150,7 @@ func (n *GetCmd) RunE(cmd *cobra.Command, args []string) error { cmd, path, inputIterators, - c8yfetcher.WithApplicationByNameFirstMatch(client, args, "id", "id"), + c8yfetcher.WithApplicationByNameFirstMatch(n.factory, args, "id", "id"), ) if err != nil { return err diff --git a/pkg/cmd/applications/list/list.auto.go b/pkg/cmd/applications/list/list.auto.go index 409984623..81467f857 100644 --- a/pkg/cmd/applications/list/list.auto.go +++ b/pkg/cmd/applications/list/list.auto.go @@ -120,7 +120,7 @@ func (n *ListCmd) RunE(cmd *cobra.Command, args []string) error { flags.WithStringValue("owner", "owner"), flags.WithStringValue("providedFor", "providedFor"), flags.WithStringValue("subscriber", "subscriber"), - c8yfetcher.WithUserByNameFirstMatch(client, args, "user", "user"), + c8yfetcher.WithUserByNameFirstMatch(n.factory, args, "user", "user"), ) if err != nil { return cmderrors.NewUserError(err) diff --git a/pkg/cmd/applications/listapplicationbinaries/listApplicationBinaries.auto.go b/pkg/cmd/applications/listapplicationbinaries/listApplicationBinaries.auto.go index 694c3e2b8..96f89691a 100644 --- a/pkg/cmd/applications/listapplicationbinaries/listApplicationBinaries.auto.go +++ b/pkg/cmd/applications/listapplicationbinaries/listApplicationBinaries.auto.go @@ -153,7 +153,7 @@ func (n *ListApplicationBinariesCmd) RunE(cmd *cobra.Command, args []string) err cmd, path, inputIterators, - c8yfetcher.WithHostedApplicationByNameFirstMatch(client, args, "id", "id"), + c8yfetcher.WithHostedApplicationByNameFirstMatch(n.factory, args, "id", "id"), ) if err != nil { return err diff --git a/pkg/cmd/applications/open/open.manual.go b/pkg/cmd/applications/open/open.manual.go index 17bfa6a2f..f337989d4 100644 --- a/pkg/cmd/applications/open/open.manual.go +++ b/pkg/cmd/applications/open/open.manual.go @@ -138,7 +138,7 @@ func (n *OpenCmd) RunE(cmd *cobra.Command, args []string) error { inputIterators, flags.WithStringValue("page", "page"), flags.WithStringValue("application", "application"), - c8yfetcher.WithDeviceByNameFirstMatch(client, args, "device", "device"), + c8yfetcher.WithDeviceByNameFirstMatch(n.factory, args, "device", "device"), ) if err != nil { return err diff --git a/pkg/cmd/applications/update/update.auto.go b/pkg/cmd/applications/update/update.auto.go index 0aab4b660..ef3330e21 100644 --- a/pkg/cmd/applications/update/update.auto.go +++ b/pkg/cmd/applications/update/update.auto.go @@ -154,7 +154,7 @@ func (n *UpdateCmd) RunE(cmd *cobra.Command, args []string) error { flags.WithStringValue("resourcesUsername", "resourcesUsername"), flags.WithStringValue("resourcesPassword", "resourcesPassword"), flags.WithStringValue("externalUrl", "externalUrl"), - cmdutil.WithTemplateValue(cfg), + cmdutil.WithTemplateValue(n.factory), flags.WithTemplateVariablesValue(), ) if err != nil { @@ -167,7 +167,7 @@ func (n *UpdateCmd) RunE(cmd *cobra.Command, args []string) error { cmd, path, inputIterators, - c8yfetcher.WithApplicationByNameFirstMatch(client, args, "id", "id"), + c8yfetcher.WithApplicationByNameFirstMatch(n.factory, args, "id", "id"), ) if err != nil { return err diff --git a/pkg/cmd/auditrecords/create/create.auto.go b/pkg/cmd/auditrecords/create/create.auto.go index b0852764a..352f3b35a 100644 --- a/pkg/cmd/auditrecords/create/create.auto.go +++ b/pkg/cmd/auditrecords/create/create.auto.go @@ -155,7 +155,7 @@ func (n *CreateCmd) RunE(cmd *cobra.Command, args []string) error { flags.WithStringValue("application", "application"), flags.WithDefaultTemplateString(` {time: _.Now('0s')}`), - cmdutil.WithTemplateValue(cfg), + cmdutil.WithTemplateValue(n.factory), flags.WithTemplateVariablesValue(), flags.WithRequiredProperties("activity", "source.id", "text", "type"), ) diff --git a/pkg/cmd/binaries/create/create.auto.go b/pkg/cmd/binaries/create/create.auto.go index 9ed0fe76b..1db92db5b 100644 --- a/pkg/cmd/binaries/create/create.auto.go +++ b/pkg/cmd/binaries/create/create.auto.go @@ -132,7 +132,7 @@ func (n *CreateCmd) RunE(cmd *cobra.Command, args []string) error { cmd, formData, inputIterators, - flags.WithFormDataFileAndInfoWithTemplateSupport(cmdutil.NewTemplateResolver(cfg), "file", "data")..., + flags.WithFormDataFileAndInfoWithTemplateSupport(cmdutil.NewTemplateResolver(n.factory), "file", "data")..., ) if err != nil { return cmderrors.NewUserError(err) @@ -146,7 +146,7 @@ func (n *CreateCmd) RunE(cmd *cobra.Command, args []string) error { inputIterators, flags.WithStringValue("name", "name"), flags.WithStringValue("type", "type"), - cmdutil.WithTemplateValue(cfg), + cmdutil.WithTemplateValue(n.factory), flags.WithTemplateVariablesValue(), ) if err != nil { diff --git a/pkg/cmd/binaries/list/list.auto.go b/pkg/cmd/binaries/list/list.auto.go index ed52a5c28..9b43757b3 100644 --- a/pkg/cmd/binaries/list/list.auto.go +++ b/pkg/cmd/binaries/list/list.auto.go @@ -112,7 +112,7 @@ func (n *ListCmd) RunE(cmd *cobra.Command, args []string) error { flags.WithStringValue("text", "text"), flags.WithStringValue("childAdditionId", "childAdditionId"), flags.WithStringValue("childAssetId", "childAssetId"), - c8yfetcher.WithDeviceByNameFirstMatch(client, args, "childDeviceId", "childDeviceId"), + c8yfetcher.WithDeviceByNameFirstMatch(n.factory, args, "childDeviceId", "childDeviceId"), ) if err != nil { return cmderrors.NewUserError(err) diff --git a/pkg/cmd/bulkoperations/create/create.auto.go b/pkg/cmd/bulkoperations/create/create.auto.go index b3d4f75b5..2bbc3571e 100644 --- a/pkg/cmd/bulkoperations/create/create.auto.go +++ b/pkg/cmd/bulkoperations/create/create.auto.go @@ -144,13 +144,13 @@ func (n *CreateCmd) RunE(cmd *cobra.Command, args []string) error { body, inputIterators, flags.WithDataFlagValue(), - c8yfetcher.WithDeviceGroupByNameFirstMatch(client, args, "group", "groupId"), + c8yfetcher.WithDeviceGroupByNameFirstMatch(n.factory, args, "group", "groupId"), flags.WithRelativeTimestamp("startDate", "startDate"), flags.WithFloatValue("creationRampSec", "creationRamp"), flags.WithDataValue("operation", "operationPrototype"), flags.WithDefaultTemplateString(` {startDate: _.Now('300s'), creationRamp: 1.000}`), - cmdutil.WithTemplateValue(cfg), + cmdutil.WithTemplateValue(n.factory), flags.WithTemplateVariablesValue(), flags.WithRequiredProperties("groupId", "startDate", "creationRamp", "operationPrototype"), ) diff --git a/pkg/cmd/bulkoperations/update/update.auto.go b/pkg/cmd/bulkoperations/update/update.auto.go index 6d9cf338f..a6971ed12 100644 --- a/pkg/cmd/bulkoperations/update/update.auto.go +++ b/pkg/cmd/bulkoperations/update/update.auto.go @@ -137,7 +137,7 @@ func (n *UpdateCmd) RunE(cmd *cobra.Command, args []string) error { inputIterators, flags.WithDataFlagValue(), flags.WithFloatValue("creationRampSec", "creationRamp"), - cmdutil.WithTemplateValue(cfg), + cmdutil.WithTemplateValue(n.factory), flags.WithTemplateVariablesValue(), flags.WithRequiredProperties("creationRamp"), ) diff --git a/pkg/cmd/cmd.go b/pkg/cmd/cmd.go index 0d612839b..df7887171 100644 --- a/pkg/cmd/cmd.go +++ b/pkg/cmd/cmd.go @@ -4,7 +4,7 @@ import ( "bytes" "errors" "fmt" - "io/ioutil" + "io" "log" "math/rand" "os" @@ -62,7 +62,7 @@ func MainRun() { } // Expand any aliases - expandedArgs, err := setArgs(rootCmd.Command) + expandedArgs, err := setArgs(rootCmd.Command, rootCmd.Factory) if err != nil { Logger.Errorf("Could not expand aliases. %s", err) os.Exit(int(cmderrors.ExitInvalidAlias)) @@ -78,6 +78,7 @@ func MainRun() { } var results []string + for aliasName, aliasValue := range aliases { if strings.HasPrefix(aliasName, toComplete) { var s string @@ -92,6 +93,33 @@ func MainRun() { results = append(results, s) } } + + // Extension Aliases + for _, ext := range rootCmd.Factory.ExtensionManager().List() { + extAliases, aliasErr := ext.Aliases() + if aliasErr == nil { + for _, iAlias := range extAliases { + aliasName := iAlias.GetName() + if strings.HasPrefix(aliasName, toComplete) { + var s string + desc := iAlias.GetDescription() + if len(desc) > 80 { + desc = desc[:80] + "..." + } + if iAlias.IsShell() { + s = fmt.Sprintf("%s\tExtension shell alias %s", aliasName, desc) + } else { + s = fmt.Sprintf("%s\tExtension alias to %s", aliasName, desc) + } + + s += fmt.Sprintf(" |%s", ext.Name()) + results = append(results, s) + } + } + } + } + // Note: Extension commands are defined in root.go + return results, cobra.ShellCompDirectiveNoFileComp } @@ -102,7 +130,7 @@ func MainRun() { err = CheckCommandError(rootCmd.Command, rootCmd.Factory, err) // Help is not really error, just a way to exit early - // after displaying help to ther user + // after displaying help to the user if errors.Is(err, cmderrors.ErrHelp) { os.Exit(int(cmderrors.ExitOK)) } @@ -126,7 +154,7 @@ func CheckCommandError(cmd *cobra.Command, f *cmdutil.Factory, err error) error if logErr != nil { log.Fatalf("Could not configure logger. %s", logErr) } - w := ioutil.Discard + w := io.Discard if errors.Is(err, cmderrors.ErrHelp) { return err @@ -187,13 +215,18 @@ func CheckCommandError(cmd *cobra.Command, f *cmdutil.Factory, err error) error return err } -func setArgs(cmd *cobra.Command) ([]string, error) { +func hasCommand(rootCmd *cobra.Command, args []string) bool { + c, _, err := rootCmd.Traverse(args) + return err == nil && c != rootCmd +} + +func setArgs(cmd *cobra.Command, cmdFactory *cmdutil.Factory) ([]string, error) { expandedArgs := []string{} + var err error if len(os.Args) > 0 { expandedArgs = os.Args[1:] } - cmd, _, err := cmd.Traverse(expandedArgs) - if err != nil || cmd == cmd.Root() { + if !hasCommand(cmd.Root(), expandedArgs) { originalArgs := expandedArgs isShell := false @@ -202,6 +235,16 @@ func setArgs(cmd *cobra.Command) ([]string, error) { for name, value := range v.GetStringMapString(config.SettingsAliases) { aliases[name] = value } + + // add any aliases defined in the extensions + for _, ext := range cmdFactory.ExtensionManager().List() { + if extAliases, err := ext.Aliases(); err == nil { + for _, alias := range extAliases { + aliases[alias.GetName()] = alias.GetCommand() + } + } + } + expandedArgs, isShell, err = expand.ExpandAlias(aliases, os.Args, nil) if err != nil { return nil, err @@ -230,6 +273,19 @@ func setArgs(cmd *cobra.Command) ([]string, error) { return nil, err } os.Exit(int(cmderrors.ExitOK)) + } else if len(expandedArgs) > 0 && !hasCommand(cmd.Root(), expandedArgs) { + + extensionManager := cmdFactory.ExtensionManager() + if found, err := extensionManager.Dispatch(expandedArgs, os.Stdin, os.Stdout, os.Stderr); err != nil { + var execError *exec.ExitError + if errors.As(err, &execError) { + return nil, cmderrors.NewUserErrorWithExitCode(cmderrors.ExitCode(execError.ExitCode()), execError) + } + fmt.Fprintf(cmdFactory.IOStreams.ErrOut, "failed to run extension: %s\n", err) + return nil, cmderrors.NewSystemError("failed to run extension") + } else if found { + return nil, cmderrors.NewErrorWithExitCode(0, nil) + } } } return expandedArgs, nil @@ -259,6 +315,34 @@ func getOutputHeaders(c *console.Console, cfg *config.Config, input []string) (h return append(bytes.Join(columns, []byte(",")), []byte("\n")...) } +// GetInitLoggerOptions create a simple logger with a best-guess log level based on the given arguments +// It will not activate on any environment variables, however it will do a simplistic parsing of +// the common logging options before the configuration has been read which enables debugging around +// the configuration and extensions etc. +func GetInitLoggerOptions(args []string) logger.Options { + color := true + level := zapcore.WarnLevel + debug := false + + for _, item := range args { + switch item { + case "--debug", "--debug=true": + level = zapcore.DebugLevel + debug = true + case "--verbose", "-v", "--verbose=true": + level = zapcore.InfoLevel + case "--noColor", "--noColor=true", "-M", "-M=true": + color = false + } + } + + return logger.Options{ + Level: level, + Debug: debug, + Color: color, + } +} + // Initialize initializes the configuration manager and c8y client func Initialize() (*root.CmdRoot, error) { @@ -270,10 +354,7 @@ func Initialize() (*root.CmdRoot, error) { var configHandler = config.NewConfig(viper.GetViper()) // init logger - logHandler = logger.NewLogger(module, logger.Options{ - Level: zapcore.WarnLevel, - Debug: false, - }) + logHandler = logger.NewLogger(module, GetInitLoggerOptions(os.Args)) if _, err := configHandler.ReadConfigFiles(nil); err != nil { logHandler.Infof("Failed to read configuration. Trying to proceed anyway. %s", err) diff --git a/pkg/cmd/cmd_test.go b/pkg/cmd/cmd_test.go index 89f9d3d35..6cbdebeaf 100644 --- a/pkg/cmd/cmd_test.go +++ b/pkg/cmd/cmd_test.go @@ -463,7 +463,7 @@ func Test_DebugStdinCommand(t *testing.T) { // stdin.Write(`{"source":{"id":"1111"}}` + "\n") cmdtext := ` - api --method GET --url /measurement/measurements --customQueryParam "dateFrom=2022-08-10T14:59:29.561+02:00" --dry=false + extension install reubenmiller/c8y-devmgmt ` cmdErr := ExecuteCmd(cmd, strings.TrimSpace(cmdtext)) diff --git a/pkg/cmd/configuration/create/create.auto.go b/pkg/cmd/configuration/create/create.auto.go index 61cc26ae2..5dbd63bcf 100644 --- a/pkg/cmd/configuration/create/create.auto.go +++ b/pkg/cmd/configuration/create/create.auto.go @@ -153,10 +153,10 @@ func (n *CreateCmd) RunE(cmd *cobra.Command, args []string) error { flags.WithStringValue("configurationType", "configurationType"), flags.WithStringValue("url", "url"), flags.WithStringValue("deviceType", "deviceType"), - c8ybinary.WithBinaryUploadURL(client, n.factory.IOStreams.ProgressIndicator(), "file", "url"), + c8ybinary.WithBinaryUploadURL(n.factory.Client, n.factory.IOStreams.ProgressIndicator(), "file", "url"), flags.WithDefaultTemplateString(` {type: 'c8y_ConfigurationDump', c8y_Global:{}}`), - cmdutil.WithTemplateValue(cfg), + cmdutil.WithTemplateValue(n.factory), flags.WithTemplateVariablesValue(), flags.WithRequiredProperties("type", "name", "url"), ) diff --git a/pkg/cmd/configuration/delete/delete.auto.go b/pkg/cmd/configuration/delete/delete.auto.go index 490af9dd3..2013a2074 100644 --- a/pkg/cmd/configuration/delete/delete.auto.go +++ b/pkg/cmd/configuration/delete/delete.auto.go @@ -150,7 +150,7 @@ func (n *DeleteCmd) RunE(cmd *cobra.Command, args []string) error { cmd, path, inputIterators, - c8yfetcher.WithConfigurationByNameFirstMatch(client, args, "id", "id"), + c8yfetcher.WithConfigurationByNameFirstMatch(n.factory, args, "id", "id"), ) if err != nil { return err diff --git a/pkg/cmd/configuration/get/get.auto.go b/pkg/cmd/configuration/get/get.auto.go index 857c6ff56..b4ab6d532 100644 --- a/pkg/cmd/configuration/get/get.auto.go +++ b/pkg/cmd/configuration/get/get.auto.go @@ -159,7 +159,7 @@ func (n *GetCmd) RunE(cmd *cobra.Command, args []string) error { cmd, path, inputIterators, - c8yfetcher.WithConfigurationByNameFirstMatch(client, args, "id", "id"), + c8yfetcher.WithConfigurationByNameFirstMatch(n.factory, args, "id", "id"), ) if err != nil { return err diff --git a/pkg/cmd/configuration/send/send.auto.go b/pkg/cmd/configuration/send/send.auto.go index e8e036ab7..aa82c9316 100644 --- a/pkg/cmd/configuration/send/send.auto.go +++ b/pkg/cmd/configuration/send/send.auto.go @@ -158,12 +158,12 @@ func (n *SendCmd) RunE(cmd *cobra.Command, args []string) error { body, inputIterators, flags.WithDataFlagValue(), - c8yfetcher.WithDeviceByNameFirstMatch(client, args, "device", "deviceId"), + c8yfetcher.WithDeviceByNameFirstMatch(n.factory, args, "device", "deviceId"), flags.WithStringValue("description", "description"), flags.WithStringValue("configurationType", "c8y_DownloadConfigFile.type"), flags.WithStringValue("url", "c8y_DownloadConfigFile.url"), - c8yfetcher.WithConfigurationByNameFirstMatch(client, args, "configuration", "__tmp_configuration"), - c8yfetcher.WithConfigurationFileData(client, "configuration", "configurationType", "url", args, "", "c8y_DownloadConfigFile"), + c8yfetcher.WithConfigurationByNameFirstMatch(n.factory, args, "configuration", "__tmp_configuration"), + c8yfetcher.WithConfigurationFileData(n.factory, "configuration", "configurationType", "url", args, "", "c8y_DownloadConfigFile"), flags.WithDefaultTemplateString(` { description: @@ -176,7 +176,7 @@ func (n *SendCmd) RunE(cmd *cobra.Command, args []string) error { c8y_DownloadConfigFile+: {name:: null}, } `), - cmdutil.WithTemplateValue(cfg), + cmdutil.WithTemplateValue(n.factory), flags.WithTemplateVariablesValue(), flags.WithRequiredProperties("deviceId"), ) diff --git a/pkg/cmd/configuration/update/update.auto.go b/pkg/cmd/configuration/update/update.auto.go index f1b57826d..5a290565b 100644 --- a/pkg/cmd/configuration/update/update.auto.go +++ b/pkg/cmd/configuration/update/update.auto.go @@ -149,8 +149,8 @@ func (n *UpdateCmd) RunE(cmd *cobra.Command, args []string) error { flags.WithStringValue("configurationType", "configurationType"), flags.WithStringValue("url", "url"), flags.WithStringValue("deviceType", "deviceType"), - c8ybinary.WithBinaryUploadURL(client, n.factory.IOStreams.ProgressIndicator(), "file", "url"), - cmdutil.WithTemplateValue(cfg), + c8ybinary.WithBinaryUploadURL(n.factory.Client, n.factory.IOStreams.ProgressIndicator(), "file", "url"), + cmdutil.WithTemplateValue(n.factory), flags.WithTemplateVariablesValue(), ) if err != nil { @@ -163,7 +163,7 @@ func (n *UpdateCmd) RunE(cmd *cobra.Command, args []string) error { cmd, path, inputIterators, - c8yfetcher.WithConfigurationByNameFirstMatch(client, args, "id", "id"), + c8yfetcher.WithConfigurationByNameFirstMatch(n.factory, args, "id", "id"), ) if err != nil { return err diff --git a/pkg/cmd/currentapplication/update/update.auto.go b/pkg/cmd/currentapplication/update/update.auto.go index df51044b1..30e7dd707 100644 --- a/pkg/cmd/currentapplication/update/update.auto.go +++ b/pkg/cmd/currentapplication/update/update.auto.go @@ -150,7 +150,7 @@ func (n *UpdateCmd) RunE(cmd *cobra.Command, args []string) error { flags.WithStringValue("resourcesUsername", "resourcesUsername"), flags.WithStringValue("resourcesPassword", "resourcesPassword"), flags.WithStringValue("externalUrl", "externalUrl"), - cmdutil.WithTemplateValue(cfg), + cmdutil.WithTemplateValue(n.factory), flags.WithTemplateVariablesValue(), ) if err != nil { diff --git a/pkg/cmd/currentuser/update/update.auto.go b/pkg/cmd/currentuser/update/update.auto.go index ab6047b12..3e5a5730b 100644 --- a/pkg/cmd/currentuser/update/update.auto.go +++ b/pkg/cmd/currentuser/update/update.auto.go @@ -146,7 +146,7 @@ func (n *UpdateCmd) RunE(cmd *cobra.Command, args []string) error { flags.WithStringValue("email", "email"), flags.WithStringValue("enabled", "enabled"), flags.WithStringValue("password", "password"), - cmdutil.WithTemplateValue(cfg), + cmdutil.WithTemplateValue(n.factory), flags.WithTemplateVariablesValue(), ) if err != nil { diff --git a/pkg/cmd/databroker/update/update.auto.go b/pkg/cmd/databroker/update/update.auto.go index 51f7f1f1a..e3f99f662 100644 --- a/pkg/cmd/databroker/update/update.auto.go +++ b/pkg/cmd/databroker/update/update.auto.go @@ -138,7 +138,7 @@ func (n *UpdateCmd) RunE(cmd *cobra.Command, args []string) error { inputIterators, flags.WithDataFlagValue(), flags.WithStringValue("status", "status"), - cmdutil.WithTemplateValue(cfg), + cmdutil.WithTemplateValue(n.factory), flags.WithTemplateVariablesValue(), flags.WithRequiredProperties("status"), ) diff --git a/pkg/cmd/datahub/jobs/create/create.auto.go b/pkg/cmd/datahub/jobs/create/create.auto.go index 6b386168c..26edd9f97 100644 --- a/pkg/cmd/datahub/jobs/create/create.auto.go +++ b/pkg/cmd/datahub/jobs/create/create.auto.go @@ -143,7 +143,7 @@ func (n *CreateCmd) RunE(cmd *cobra.Command, args []string) error { flags.WithDataFlagValue(), flags.WithStringValue("sql", "sql"), flags.WithStringSliceValues("context", "context", ""), - cmdutil.WithTemplateValue(cfg), + cmdutil.WithTemplateValue(n.factory), flags.WithTemplateVariablesValue(), flags.WithRequiredProperties("sql"), ) diff --git a/pkg/cmd/datahub/query/query.auto.go b/pkg/cmd/datahub/query/query.auto.go index 65b5e543d..de937d81e 100644 --- a/pkg/cmd/datahub/query/query.auto.go +++ b/pkg/cmd/datahub/query/query.auto.go @@ -152,7 +152,7 @@ func (n *QueryCmd) RunE(cmd *cobra.Command, args []string) error { flags.WithStringValue("sql", "sql"), flags.WithIntValue("limit", "limit"), flags.WithStringValue("format", "format"), - cmdutil.WithTemplateValue(cfg), + cmdutil.WithTemplateValue(n.factory), flags.WithTemplateVariablesValue(), flags.WithRequiredProperties("sql", "limit"), ) diff --git a/pkg/cmd/devicegroups/assigndevice/assignDevice.auto.go b/pkg/cmd/devicegroups/assigndevice/assignDevice.auto.go index c5d958a55..cfb0122e0 100644 --- a/pkg/cmd/devicegroups/assigndevice/assignDevice.auto.go +++ b/pkg/cmd/devicegroups/assigndevice/assignDevice.auto.go @@ -148,8 +148,8 @@ func (n *AssignDeviceCmd) RunE(cmd *cobra.Command, args []string) error { body, inputIterators, flags.WithDataFlagValue(), - c8yfetcher.WithDeviceByNameFirstMatch(client, args, "newChildDevice", "managedObject.id"), - cmdutil.WithTemplateValue(cfg), + c8yfetcher.WithDeviceByNameFirstMatch(n.factory, args, "newChildDevice", "managedObject.id"), + cmdutil.WithTemplateValue(n.factory), flags.WithTemplateVariablesValue(), ) if err != nil { @@ -162,7 +162,7 @@ func (n *AssignDeviceCmd) RunE(cmd *cobra.Command, args []string) error { cmd, path, inputIterators, - c8yfetcher.WithDeviceGroupByNameFirstMatch(client, args, "group", "id"), + c8yfetcher.WithDeviceGroupByNameFirstMatch(n.factory, args, "group", "id"), ) if err != nil { return err diff --git a/pkg/cmd/devicegroups/assigngroup/assignGroup.auto.go b/pkg/cmd/devicegroups/assigngroup/assignGroup.auto.go index b528ab20e..b90241406 100644 --- a/pkg/cmd/devicegroups/assigngroup/assignGroup.auto.go +++ b/pkg/cmd/devicegroups/assigngroup/assignGroup.auto.go @@ -148,8 +148,8 @@ func (n *AssignGroupCmd) RunE(cmd *cobra.Command, args []string) error { body, inputIterators, flags.WithDataFlagValue(), - c8yfetcher.WithDeviceGroupByNameFirstMatch(client, args, "newChildGroup", "managedObject.id"), - cmdutil.WithTemplateValue(cfg), + c8yfetcher.WithDeviceGroupByNameFirstMatch(n.factory, args, "newChildGroup", "managedObject.id"), + cmdutil.WithTemplateValue(n.factory), flags.WithTemplateVariablesValue(), ) if err != nil { @@ -162,7 +162,7 @@ func (n *AssignGroupCmd) RunE(cmd *cobra.Command, args []string) error { cmd, path, inputIterators, - c8yfetcher.WithDeviceGroupByNameFirstMatch(client, args, "group", "id"), + c8yfetcher.WithDeviceGroupByNameFirstMatch(n.factory, args, "group", "id"), ) if err != nil { return err diff --git a/pkg/cmd/devicegroups/children/assign/assign.auto.go b/pkg/cmd/devicegroups/children/assign/assign.auto.go index 86951ed3b..74061b755 100644 --- a/pkg/cmd/devicegroups/children/assign/assign.auto.go +++ b/pkg/cmd/devicegroups/children/assign/assign.auto.go @@ -145,7 +145,7 @@ func (n *AssignCmd) RunE(cmd *cobra.Command, args []string) error { flags.WithOverrideValue("child", "managedObject.id"), flags.WithDataFlagValue(), flags.WithStringValue("child", "managedObject.id"), - cmdutil.WithTemplateValue(cfg), + cmdutil.WithTemplateValue(n.factory), flags.WithTemplateVariablesValue(), ) if err != nil { @@ -158,7 +158,7 @@ func (n *AssignCmd) RunE(cmd *cobra.Command, args []string) error { cmd, path, inputIterators, - c8yfetcher.WithDeviceGroupByNameFirstMatch(client, args, "id", "id"), + c8yfetcher.WithDeviceGroupByNameFirstMatch(n.factory, args, "id", "id"), flags.WithInventoryChildType("childType", "childType"), ) if err != nil { diff --git a/pkg/cmd/devicegroups/children/create/create.auto.go b/pkg/cmd/devicegroups/children/create/create.auto.go index 07b1588d3..919f6f53d 100644 --- a/pkg/cmd/devicegroups/children/create/create.auto.go +++ b/pkg/cmd/devicegroups/children/create/create.auto.go @@ -143,7 +143,7 @@ func (n *CreateCmd) RunE(cmd *cobra.Command, args []string) error { inputIterators, flags.WithDataFlagValue(), flags.WithOptionalFragment("global", "c8y_Global", ""), - cmdutil.WithTemplateValue(cfg), + cmdutil.WithTemplateValue(n.factory), flags.WithTemplateVariablesValue(), ) if err != nil { @@ -156,7 +156,7 @@ func (n *CreateCmd) RunE(cmd *cobra.Command, args []string) error { cmd, path, inputIterators, - c8yfetcher.WithDeviceGroupByNameFirstMatch(client, args, "id", "id"), + c8yfetcher.WithDeviceGroupByNameFirstMatch(n.factory, args, "id", "id"), flags.WithInventoryChildType("childType", "childType"), ) if err != nil { diff --git a/pkg/cmd/devicegroups/children/get/get.auto.go b/pkg/cmd/devicegroups/children/get/get.auto.go index f4d84952b..d1784c66c 100644 --- a/pkg/cmd/devicegroups/children/get/get.auto.go +++ b/pkg/cmd/devicegroups/children/get/get.auto.go @@ -159,9 +159,9 @@ func (n *GetCmd) RunE(cmd *cobra.Command, args []string) error { cmd, path, inputIterators, - c8yfetcher.WithDeviceGroupByNameFirstMatch(client, args, "id", "id"), + c8yfetcher.WithDeviceGroupByNameFirstMatch(n.factory, args, "id", "id"), flags.WithInventoryChildType("childType", "childType"), - c8yfetcher.WithDeviceGroupByNameFirstMatch(client, args, "child", "child"), + c8yfetcher.WithDeviceGroupByNameFirstMatch(n.factory, args, "child", "child"), ) if err != nil { return err diff --git a/pkg/cmd/devicegroups/children/list/list.auto.go b/pkg/cmd/devicegroups/children/list/list.auto.go index 294310ea1..edfa3420e 100644 --- a/pkg/cmd/devicegroups/children/list/list.auto.go +++ b/pkg/cmd/devicegroups/children/list/list.auto.go @@ -172,7 +172,7 @@ func (n *ListCmd) RunE(cmd *cobra.Command, args []string) error { cmd, path, inputIterators, - c8yfetcher.WithDeviceGroupByNameFirstMatch(client, args, "id", "id"), + c8yfetcher.WithDeviceGroupByNameFirstMatch(n.factory, args, "id", "id"), flags.WithInventoryChildType("childType", "childType"), ) if err != nil { diff --git a/pkg/cmd/devicegroups/children/unassign/unassign.auto.go b/pkg/cmd/devicegroups/children/unassign/unassign.auto.go index fae89117f..e3519a65e 100644 --- a/pkg/cmd/devicegroups/children/unassign/unassign.auto.go +++ b/pkg/cmd/devicegroups/children/unassign/unassign.auto.go @@ -154,7 +154,7 @@ func (n *UnassignCmd) RunE(cmd *cobra.Command, args []string) error { cmd, path, inputIterators, - c8yfetcher.WithDeviceGroupByNameFirstMatch(client, args, "id", "id"), + c8yfetcher.WithDeviceGroupByNameFirstMatch(n.factory, args, "id", "id"), flags.WithInventoryChildType("childType", "childType"), flags.WithStringValue("child", "child"), ) diff --git a/pkg/cmd/devicegroups/create/create.auto.go b/pkg/cmd/devicegroups/create/create.auto.go index 241d26c5d..846deb3b9 100644 --- a/pkg/cmd/devicegroups/create/create.auto.go +++ b/pkg/cmd/devicegroups/create/create.auto.go @@ -145,7 +145,7 @@ func (n *CreateCmd) RunE(cmd *cobra.Command, args []string) error { flags.WithStringValue("type", "type"), flags.WithDefaultTemplateString(` {type: 'c8y_DeviceGroup', c8y_IsDeviceGroup: {}}`), - cmdutil.WithTemplateValue(cfg), + cmdutil.WithTemplateValue(n.factory), flags.WithTemplateVariablesValue(), flags.WithRequiredProperties("name"), ) diff --git a/pkg/cmd/devicegroups/delete/delete.auto.go b/pkg/cmd/devicegroups/delete/delete.auto.go index e05ed7f01..e01fa16f1 100644 --- a/pkg/cmd/devicegroups/delete/delete.auto.go +++ b/pkg/cmd/devicegroups/delete/delete.auto.go @@ -149,7 +149,7 @@ func (n *DeleteCmd) RunE(cmd *cobra.Command, args []string) error { cmd, path, inputIterators, - c8yfetcher.WithDeviceGroupByNameFirstMatch(client, args, "id", "id"), + c8yfetcher.WithDeviceGroupByNameFirstMatch(n.factory, args, "id", "id"), ) if err != nil { return err diff --git a/pkg/cmd/devicegroups/get/get.auto.go b/pkg/cmd/devicegroups/get/get.auto.go index 280afebd1..2d49064ec 100644 --- a/pkg/cmd/devicegroups/get/get.auto.go +++ b/pkg/cmd/devicegroups/get/get.auto.go @@ -161,7 +161,7 @@ func (n *GetCmd) RunE(cmd *cobra.Command, args []string) error { cmd, path, inputIterators, - c8yfetcher.WithDeviceGroupByNameFirstMatch(client, args, "id", "id"), + c8yfetcher.WithDeviceGroupByNameFirstMatch(n.factory, args, "id", "id"), ) if err != nil { return err diff --git a/pkg/cmd/devicegroups/list/list.auto.go b/pkg/cmd/devicegroups/list/list.auto.go index 8eed99644..40057021f 100644 --- a/pkg/cmd/devicegroups/list/list.auto.go +++ b/pkg/cmd/devicegroups/list/list.auto.go @@ -124,7 +124,7 @@ func (n *ListCmd) RunE(cmd *cobra.Command, args []string) error { flags.WithStringValue("fragmentType", "fragmentType", "has(%s)"), flags.WithStringValue("owner", "owner", "(owner eq '%s')"), flags.WithDefaultBoolValue("excludeRootGroup", "excludeRootGroup", "not(type eq 'c8y_DeviceGroup')"), - c8yfetcher.WithDeviceGroupByNameFirstMatch(client, args, "group", "group", "bygroupid(%s)"), + c8yfetcher.WithDeviceGroupByNameFirstMatch(n.factory, args, "group", "group", "bygroupid(%s)"), }, "query", ), diff --git a/pkg/cmd/devicegroups/listassets/listAssets.auto.go b/pkg/cmd/devicegroups/listassets/listAssets.auto.go index e9e08d65a..5eb61f01e 100644 --- a/pkg/cmd/devicegroups/listassets/listAssets.auto.go +++ b/pkg/cmd/devicegroups/listassets/listAssets.auto.go @@ -159,7 +159,7 @@ func (n *ListAssetsCmd) RunE(cmd *cobra.Command, args []string) error { cmd, path, inputIterators, - c8yfetcher.WithDeviceGroupByNameFirstMatch(client, args, "id", "id"), + c8yfetcher.WithDeviceGroupByNameFirstMatch(n.factory, args, "id", "id"), ) if err != nil { return err diff --git a/pkg/cmd/devicegroups/unassigndevice/unassignDevice.auto.go b/pkg/cmd/devicegroups/unassigndevice/unassignDevice.auto.go index 41aa5e7b8..bfed647ec 100644 --- a/pkg/cmd/devicegroups/unassigndevice/unassignDevice.auto.go +++ b/pkg/cmd/devicegroups/unassigndevice/unassignDevice.auto.go @@ -154,8 +154,8 @@ func (n *UnassignDeviceCmd) RunE(cmd *cobra.Command, args []string) error { cmd, path, inputIterators, - c8yfetcher.WithDeviceGroupByNameFirstMatch(client, args, "group", "group"), - c8yfetcher.WithDeviceByNameFirstMatch(client, args, "childDevice", "reference"), + c8yfetcher.WithDeviceGroupByNameFirstMatch(n.factory, args, "group", "group"), + c8yfetcher.WithDeviceByNameFirstMatch(n.factory, args, "childDevice", "reference"), ) if err != nil { return err diff --git a/pkg/cmd/devicegroups/unassigngroup/unassignGroup.auto.go b/pkg/cmd/devicegroups/unassigngroup/unassignGroup.auto.go index 09b399cf4..d53ae365e 100644 --- a/pkg/cmd/devicegroups/unassigngroup/unassignGroup.auto.go +++ b/pkg/cmd/devicegroups/unassigngroup/unassignGroup.auto.go @@ -154,8 +154,8 @@ func (n *UnassignGroupCmd) RunE(cmd *cobra.Command, args []string) error { cmd, path, inputIterators, - c8yfetcher.WithDeviceGroupByNameFirstMatch(client, args, "id", "id"), - c8yfetcher.WithDeviceGroupByNameFirstMatch(client, args, "child", "child"), + c8yfetcher.WithDeviceGroupByNameFirstMatch(n.factory, args, "id", "id"), + c8yfetcher.WithDeviceGroupByNameFirstMatch(n.factory, args, "child", "child"), ) if err != nil { return err diff --git a/pkg/cmd/devicegroups/update/update.auto.go b/pkg/cmd/devicegroups/update/update.auto.go index ce55e4cc0..a57b78c84 100644 --- a/pkg/cmd/devicegroups/update/update.auto.go +++ b/pkg/cmd/devicegroups/update/update.auto.go @@ -140,7 +140,7 @@ func (n *UpdateCmd) RunE(cmd *cobra.Command, args []string) error { inputIterators, flags.WithDataFlagValue(), flags.WithStringValue("name", "name"), - cmdutil.WithTemplateValue(cfg), + cmdutil.WithTemplateValue(n.factory), flags.WithTemplateVariablesValue(), ) if err != nil { @@ -153,7 +153,7 @@ func (n *UpdateCmd) RunE(cmd *cobra.Command, args []string) error { cmd, path, inputIterators, - c8yfetcher.WithDeviceGroupByNameFirstMatch(client, args, "id", "id"), + c8yfetcher.WithDeviceGroupByNameFirstMatch(n.factory, args, "id", "id"), ) if err != nil { return err diff --git a/pkg/cmd/devicemanagement/certificates/create/create.auto.go b/pkg/cmd/devicemanagement/certificates/create/create.auto.go index 4076cbdd2..1afa93aae 100644 --- a/pkg/cmd/devicemanagement/certificates/create/create.auto.go +++ b/pkg/cmd/devicemanagement/certificates/create/create.auto.go @@ -150,7 +150,7 @@ func (n *CreateCmd) RunE(cmd *cobra.Command, args []string) error { flags.WithStringValue("status", "status"), flags.WithCertificateFile("file", "certInPemFormat"), flags.WithBoolValue("autoRegistrationEnabled", "autoRegistrationEnabled", ""), - cmdutil.WithTemplateValue(cfg), + cmdutil.WithTemplateValue(n.factory), flags.WithTemplateVariablesValue(), flags.WithRequiredProperties("name", "certInPemFormat", "status"), ) @@ -164,7 +164,7 @@ func (n *CreateCmd) RunE(cmd *cobra.Command, args []string) error { cmd, path, inputIterators, - flags.WithStringDefaultValue(client.TenantName, "tenant", "tenant"), + flags.WithStringDefaultValue(n.factory.GetTenant(), "tenant", "tenant"), ) if err != nil { return err diff --git a/pkg/cmd/devicemanagement/certificates/delete/delete.auto.go b/pkg/cmd/devicemanagement/certificates/delete/delete.auto.go index 3a2914a0b..fd0b36bf8 100644 --- a/pkg/cmd/devicemanagement/certificates/delete/delete.auto.go +++ b/pkg/cmd/devicemanagement/certificates/delete/delete.auto.go @@ -152,8 +152,8 @@ func (n *DeleteCmd) RunE(cmd *cobra.Command, args []string) error { cmd, path, inputIterators, - c8yfetcher.WithCertificateByNameFirstMatch(client, args, "id", "id"), - flags.WithStringDefaultValue(client.TenantName, "tenant", "tenant"), + c8yfetcher.WithCertificateByNameFirstMatch(n.factory, args, "id", "id"), + flags.WithStringDefaultValue(n.factory.GetTenant(), "tenant", "tenant"), ) if err != nil { return err diff --git a/pkg/cmd/devicemanagement/certificates/get/get.auto.go b/pkg/cmd/devicemanagement/certificates/get/get.auto.go index 4f08253d8..b472b1329 100644 --- a/pkg/cmd/devicemanagement/certificates/get/get.auto.go +++ b/pkg/cmd/devicemanagement/certificates/get/get.auto.go @@ -155,8 +155,8 @@ func (n *GetCmd) RunE(cmd *cobra.Command, args []string) error { cmd, path, inputIterators, - c8yfetcher.WithCertificateByNameFirstMatch(client, args, "id", "id"), - flags.WithStringDefaultValue(client.TenantName, "tenant", "tenant"), + c8yfetcher.WithCertificateByNameFirstMatch(n.factory, args, "id", "id"), + flags.WithStringDefaultValue(n.factory.GetTenant(), "tenant", "tenant"), ) if err != nil { return err diff --git a/pkg/cmd/devicemanagement/certificates/list/list.auto.go b/pkg/cmd/devicemanagement/certificates/list/list.auto.go index d36981a58..6a48b42ee 100644 --- a/pkg/cmd/devicemanagement/certificates/list/list.auto.go +++ b/pkg/cmd/devicemanagement/certificates/list/list.auto.go @@ -152,7 +152,7 @@ func (n *ListCmd) RunE(cmd *cobra.Command, args []string) error { cmd, path, inputIterators, - flags.WithStringDefaultValue(client.TenantName, "tenant", "tenant"), + flags.WithStringDefaultValue(n.factory.GetTenant(), "tenant", "tenant"), ) if err != nil { return err diff --git a/pkg/cmd/devicemanagement/certificates/update/update.auto.go b/pkg/cmd/devicemanagement/certificates/update/update.auto.go index 0648731c4..2d12c94b6 100644 --- a/pkg/cmd/devicemanagement/certificates/update/update.auto.go +++ b/pkg/cmd/devicemanagement/certificates/update/update.auto.go @@ -149,7 +149,7 @@ func (n *UpdateCmd) RunE(cmd *cobra.Command, args []string) error { flags.WithStringValue("name", "name"), flags.WithStringValue("status", "status"), flags.WithBoolValue("autoRegistrationEnabled", "autoRegistrationEnabled", ""), - cmdutil.WithTemplateValue(cfg), + cmdutil.WithTemplateValue(n.factory), flags.WithTemplateVariablesValue(), ) if err != nil { @@ -162,8 +162,8 @@ func (n *UpdateCmd) RunE(cmd *cobra.Command, args []string) error { cmd, path, inputIterators, - c8yfetcher.WithCertificateByNameFirstMatch(client, args, "id", "id"), - flags.WithStringDefaultValue(client.TenantName, "tenant", "tenant"), + c8yfetcher.WithCertificateByNameFirstMatch(n.factory, args, "id", "id"), + flags.WithStringDefaultValue(n.factory.GetTenant(), "tenant", "tenant"), ) if err != nil { return err diff --git a/pkg/cmd/deviceprofiles/create/create.auto.go b/pkg/cmd/deviceprofiles/create/create.auto.go index 7199c9ef1..ba2fb7d07 100644 --- a/pkg/cmd/deviceprofiles/create/create.auto.go +++ b/pkg/cmd/deviceprofiles/create/create.auto.go @@ -140,7 +140,7 @@ func (n *CreateCmd) RunE(cmd *cobra.Command, args []string) error { flags.WithStringValue("deviceType", "c8y_Filter.type"), flags.WithDefaultTemplateString(` {type: 'c8y_Profile', c8y_DeviceProfile:{}, c8y_Filter:{}}`), - cmdutil.WithTemplateValue(cfg), + cmdutil.WithTemplateValue(n.factory), flags.WithTemplateVariablesValue(), flags.WithRequiredProperties("type", "name"), ) diff --git a/pkg/cmd/deviceprofiles/delete/delete.auto.go b/pkg/cmd/deviceprofiles/delete/delete.auto.go index 796628596..741de6333 100644 --- a/pkg/cmd/deviceprofiles/delete/delete.auto.go +++ b/pkg/cmd/deviceprofiles/delete/delete.auto.go @@ -145,7 +145,7 @@ func (n *DeleteCmd) RunE(cmd *cobra.Command, args []string) error { cmd, path, inputIterators, - c8yfetcher.WithDeviceProfileByNameFirstMatch(client, args, "id", "id"), + c8yfetcher.WithDeviceProfileByNameFirstMatch(n.factory, args, "id", "id"), ) if err != nil { return err diff --git a/pkg/cmd/deviceprofiles/get/get.auto.go b/pkg/cmd/deviceprofiles/get/get.auto.go index 4242d568f..e8a85cba6 100644 --- a/pkg/cmd/deviceprofiles/get/get.auto.go +++ b/pkg/cmd/deviceprofiles/get/get.auto.go @@ -159,7 +159,7 @@ func (n *GetCmd) RunE(cmd *cobra.Command, args []string) error { cmd, path, inputIterators, - c8yfetcher.WithDeviceProfileByNameFirstMatch(client, args, "id", "id"), + c8yfetcher.WithDeviceProfileByNameFirstMatch(n.factory, args, "id", "id"), ) if err != nil { return err diff --git a/pkg/cmd/deviceprofiles/update/update.auto.go b/pkg/cmd/deviceprofiles/update/update.auto.go index a8793762f..3c5dd38fb 100644 --- a/pkg/cmd/deviceprofiles/update/update.auto.go +++ b/pkg/cmd/deviceprofiles/update/update.auto.go @@ -140,7 +140,7 @@ func (n *UpdateCmd) RunE(cmd *cobra.Command, args []string) error { flags.WithDataFlagValue(), flags.WithStringValue("newName", "name"), flags.WithStringValue("deviceType", "c8y_Filter.type"), - cmdutil.WithTemplateValue(cfg), + cmdutil.WithTemplateValue(n.factory), flags.WithTemplateVariablesValue(), ) if err != nil { @@ -153,7 +153,7 @@ func (n *UpdateCmd) RunE(cmd *cobra.Command, args []string) error { cmd, path, inputIterators, - c8yfetcher.WithDeviceProfileByNameFirstMatch(client, args, "id", "id"), + c8yfetcher.WithDeviceProfileByNameFirstMatch(n.factory, args, "id", "id"), ) if err != nil { return err diff --git a/pkg/cmd/deviceregistration/approve/approve.auto.go b/pkg/cmd/deviceregistration/approve/approve.auto.go index f6d91eb37..dcd5421c3 100644 --- a/pkg/cmd/deviceregistration/approve/approve.auto.go +++ b/pkg/cmd/deviceregistration/approve/approve.auto.go @@ -140,7 +140,7 @@ func (n *ApproveCmd) RunE(cmd *cobra.Command, args []string) error { flags.WithStringValue("status", "status"), flags.WithDefaultTemplateString(` {status: 'ACCEPTED'}`), - cmdutil.WithTemplateValue(cfg), + cmdutil.WithTemplateValue(n.factory), flags.WithTemplateVariablesValue(), ) if err != nil { diff --git a/pkg/cmd/deviceregistration/getcredentials/getCredentials.auto.go b/pkg/cmd/deviceregistration/getcredentials/getCredentials.auto.go index 2a8ee3ac2..6e1196fb8 100644 --- a/pkg/cmd/deviceregistration/getcredentials/getCredentials.auto.go +++ b/pkg/cmd/deviceregistration/getcredentials/getCredentials.auto.go @@ -136,7 +136,7 @@ func (n *GetCredentialsCmd) RunE(cmd *cobra.Command, args []string) error { inputIterators, flags.WithDataFlagValue(), c8yfetcher.WithIDSlice(args, "id", "id"), - cmdutil.WithTemplateValue(cfg), + cmdutil.WithTemplateValue(n.factory), flags.WithTemplateVariablesValue(), ) if err != nil { diff --git a/pkg/cmd/deviceregistration/register/register.auto.go b/pkg/cmd/deviceregistration/register/register.auto.go index 9bb58563a..24b2b1766 100644 --- a/pkg/cmd/deviceregistration/register/register.auto.go +++ b/pkg/cmd/deviceregistration/register/register.auto.go @@ -135,7 +135,7 @@ func (n *RegisterCmd) RunE(cmd *cobra.Command, args []string) error { inputIterators, flags.WithDataFlagValue(), c8yfetcher.WithIDSlice(args, "id", "id"), - cmdutil.WithTemplateValue(cfg), + cmdutil.WithTemplateValue(n.factory), flags.WithTemplateVariablesValue(), ) if err != nil { diff --git a/pkg/cmd/devices/assignchild/assignChild.auto.go b/pkg/cmd/devices/assignchild/assignChild.auto.go index 7f3d57502..7b7ca7834 100644 --- a/pkg/cmd/devices/assignchild/assignChild.auto.go +++ b/pkg/cmd/devices/assignchild/assignChild.auto.go @@ -145,8 +145,8 @@ func (n *AssignChildCmd) RunE(cmd *cobra.Command, args []string) error { body, inputIterators, flags.WithDataFlagValue(), - c8yfetcher.WithDeviceByNameFirstMatch(client, args, "newChild", "managedObject.id"), - cmdutil.WithTemplateValue(cfg), + c8yfetcher.WithDeviceByNameFirstMatch(n.factory, args, "newChild", "managedObject.id"), + cmdutil.WithTemplateValue(n.factory), flags.WithTemplateVariablesValue(), ) if err != nil { @@ -159,7 +159,7 @@ func (n *AssignChildCmd) RunE(cmd *cobra.Command, args []string) error { cmd, path, inputIterators, - c8yfetcher.WithDeviceByNameFirstMatch(client, args, "device", "device"), + c8yfetcher.WithDeviceByNameFirstMatch(n.factory, args, "device", "device"), ) if err != nil { return err diff --git a/pkg/cmd/devices/availability/get/get.auto.go b/pkg/cmd/devices/availability/get/get.auto.go index d03926f18..4b0413f9f 100644 --- a/pkg/cmd/devices/availability/get/get.auto.go +++ b/pkg/cmd/devices/availability/get/get.auto.go @@ -156,7 +156,7 @@ func (n *GetCmd) RunE(cmd *cobra.Command, args []string) error { cmd, path, inputIterators, - c8yfetcher.WithDeviceByNameFirstMatch(client, args, "id", "id"), + c8yfetcher.WithDeviceByNameFirstMatch(n.factory, args, "id", "id"), ) if err != nil { return err diff --git a/pkg/cmd/devices/availability/set/set.auto.go b/pkg/cmd/devices/availability/set/set.auto.go index d5661f0ac..d5e607ec9 100644 --- a/pkg/cmd/devices/availability/set/set.auto.go +++ b/pkg/cmd/devices/availability/set/set.auto.go @@ -145,7 +145,7 @@ func (n *SetCmd) RunE(cmd *cobra.Command, args []string) error { inputIterators, flags.WithDataFlagValue(), flags.WithIntValue("interval", "c8y_RequiredAvailability.responseInterval"), - cmdutil.WithTemplateValue(cfg), + cmdutil.WithTemplateValue(n.factory), flags.WithTemplateVariablesValue(), ) if err != nil { @@ -158,7 +158,7 @@ func (n *SetCmd) RunE(cmd *cobra.Command, args []string) error { cmd, path, inputIterators, - c8yfetcher.WithDeviceByNameFirstMatch(client, args, "id", "id"), + c8yfetcher.WithDeviceByNameFirstMatch(n.factory, args, "id", "id"), ) if err != nil { return err diff --git a/pkg/cmd/devices/children/assign/assign.auto.go b/pkg/cmd/devices/children/assign/assign.auto.go index 52a648a4f..f6ec7b4c2 100644 --- a/pkg/cmd/devices/children/assign/assign.auto.go +++ b/pkg/cmd/devices/children/assign/assign.auto.go @@ -145,7 +145,7 @@ func (n *AssignCmd) RunE(cmd *cobra.Command, args []string) error { flags.WithOverrideValue("child", "managedObject.id"), flags.WithDataFlagValue(), flags.WithStringValue("child", "managedObject.id"), - cmdutil.WithTemplateValue(cfg), + cmdutil.WithTemplateValue(n.factory), flags.WithTemplateVariablesValue(), ) if err != nil { @@ -158,7 +158,7 @@ func (n *AssignCmd) RunE(cmd *cobra.Command, args []string) error { cmd, path, inputIterators, - c8yfetcher.WithDeviceByNameFirstMatch(client, args, "id", "id"), + c8yfetcher.WithDeviceByNameFirstMatch(n.factory, args, "id", "id"), flags.WithInventoryChildType("childType", "childType"), ) if err != nil { diff --git a/pkg/cmd/devices/children/create/create.auto.go b/pkg/cmd/devices/children/create/create.auto.go index 615cc71ff..b29bc9150 100644 --- a/pkg/cmd/devices/children/create/create.auto.go +++ b/pkg/cmd/devices/children/create/create.auto.go @@ -143,7 +143,7 @@ func (n *CreateCmd) RunE(cmd *cobra.Command, args []string) error { inputIterators, flags.WithDataFlagValue(), flags.WithOptionalFragment("global", "c8y_Global", ""), - cmdutil.WithTemplateValue(cfg), + cmdutil.WithTemplateValue(n.factory), flags.WithTemplateVariablesValue(), ) if err != nil { @@ -156,7 +156,7 @@ func (n *CreateCmd) RunE(cmd *cobra.Command, args []string) error { cmd, path, inputIterators, - c8yfetcher.WithDeviceByNameFirstMatch(client, args, "id", "id"), + c8yfetcher.WithDeviceByNameFirstMatch(n.factory, args, "id", "id"), flags.WithInventoryChildType("childType", "childType"), ) if err != nil { diff --git a/pkg/cmd/devices/children/get/get.auto.go b/pkg/cmd/devices/children/get/get.auto.go index 6f1bc7fa2..247ae2a8e 100644 --- a/pkg/cmd/devices/children/get/get.auto.go +++ b/pkg/cmd/devices/children/get/get.auto.go @@ -159,9 +159,9 @@ func (n *GetCmd) RunE(cmd *cobra.Command, args []string) error { cmd, path, inputIterators, - c8yfetcher.WithDeviceByNameFirstMatch(client, args, "id", "id"), + c8yfetcher.WithDeviceByNameFirstMatch(n.factory, args, "id", "id"), flags.WithInventoryChildType("childType", "childType"), - c8yfetcher.WithDeviceByNameFirstMatch(client, args, "child", "child"), + c8yfetcher.WithDeviceByNameFirstMatch(n.factory, args, "child", "child"), ) if err != nil { return err diff --git a/pkg/cmd/devices/children/list/list.auto.go b/pkg/cmd/devices/children/list/list.auto.go index 5413575a6..3e8917ea1 100644 --- a/pkg/cmd/devices/children/list/list.auto.go +++ b/pkg/cmd/devices/children/list/list.auto.go @@ -172,7 +172,7 @@ func (n *ListCmd) RunE(cmd *cobra.Command, args []string) error { cmd, path, inputIterators, - c8yfetcher.WithDeviceByNameFirstMatch(client, args, "id", "id"), + c8yfetcher.WithDeviceByNameFirstMatch(n.factory, args, "id", "id"), flags.WithInventoryChildType("childType", "childType"), ) if err != nil { diff --git a/pkg/cmd/devices/children/unassign/unassign.auto.go b/pkg/cmd/devices/children/unassign/unassign.auto.go index 13a4a3455..b32c3ad54 100644 --- a/pkg/cmd/devices/children/unassign/unassign.auto.go +++ b/pkg/cmd/devices/children/unassign/unassign.auto.go @@ -154,7 +154,7 @@ func (n *UnassignCmd) RunE(cmd *cobra.Command, args []string) error { cmd, path, inputIterators, - c8yfetcher.WithDeviceByNameFirstMatch(client, args, "id", "id"), + c8yfetcher.WithDeviceByNameFirstMatch(n.factory, args, "id", "id"), flags.WithInventoryChildType("childType", "childType"), flags.WithStringValue("child", "child"), ) diff --git a/pkg/cmd/devices/create/create.auto.go b/pkg/cmd/devices/create/create.auto.go index c3bbae04c..41274e7c5 100644 --- a/pkg/cmd/devices/create/create.auto.go +++ b/pkg/cmd/devices/create/create.auto.go @@ -144,7 +144,7 @@ func (n *CreateCmd) RunE(cmd *cobra.Command, args []string) error { flags.WithStringValue("type", "type"), flags.WithRequiredTemplateString(` {c8y_IsDevice: {}}`), - cmdutil.WithTemplateValue(cfg), + cmdutil.WithTemplateValue(n.factory), flags.WithTemplateVariablesValue(), flags.WithRequiredProperties("name"), ) diff --git a/pkg/cmd/devices/delete/delete.auto.go b/pkg/cmd/devices/delete/delete.auto.go index ff1bfd5ef..639619ebe 100644 --- a/pkg/cmd/devices/delete/delete.auto.go +++ b/pkg/cmd/devices/delete/delete.auto.go @@ -157,7 +157,7 @@ func (n *DeleteCmd) RunE(cmd *cobra.Command, args []string) error { cmd, path, inputIterators, - c8yfetcher.WithDeviceByNameFirstMatch(client, args, "id", "id"), + c8yfetcher.WithDeviceByNameFirstMatch(n.factory, args, "id", "id"), ) if err != nil { return err diff --git a/pkg/cmd/devices/get/get.auto.go b/pkg/cmd/devices/get/get.auto.go index 145421e11..a41f2b9b4 100644 --- a/pkg/cmd/devices/get/get.auto.go +++ b/pkg/cmd/devices/get/get.auto.go @@ -160,7 +160,7 @@ func (n *GetCmd) RunE(cmd *cobra.Command, args []string) error { cmd, path, inputIterators, - c8yfetcher.WithDeviceByNameFirstMatch(client, args, "id", "id"), + c8yfetcher.WithDeviceByNameFirstMatch(n.factory, args, "id", "id"), ) if err != nil { return err diff --git a/pkg/cmd/devices/getchild/getChild.auto.go b/pkg/cmd/devices/getchild/getChild.auto.go index e048fe794..73df6327a 100644 --- a/pkg/cmd/devices/getchild/getChild.auto.go +++ b/pkg/cmd/devices/getchild/getChild.auto.go @@ -158,8 +158,8 @@ func (n *GetChildCmd) RunE(cmd *cobra.Command, args []string) error { cmd, path, inputIterators, - c8yfetcher.WithDeviceByNameFirstMatch(client, args, "device", "device"), - c8yfetcher.WithDeviceByNameFirstMatch(client, args, "reference", "reference"), + c8yfetcher.WithDeviceByNameFirstMatch(n.factory, args, "device", "device"), + c8yfetcher.WithDeviceByNameFirstMatch(n.factory, args, "reference", "reference"), ) if err != nil { return err diff --git a/pkg/cmd/devices/getsupportedmeasurements/getSupportedMeasurements.auto.go b/pkg/cmd/devices/getsupportedmeasurements/getSupportedMeasurements.auto.go index 093caf36c..cbee9513d 100644 --- a/pkg/cmd/devices/getsupportedmeasurements/getSupportedMeasurements.auto.go +++ b/pkg/cmd/devices/getsupportedmeasurements/getSupportedMeasurements.auto.go @@ -153,7 +153,7 @@ func (n *GetSupportedMeasurementsCmd) RunE(cmd *cobra.Command, args []string) er cmd, path, inputIterators, - c8yfetcher.WithDeviceByNameFirstMatch(client, args, "device", "device"), + c8yfetcher.WithDeviceByNameFirstMatch(n.factory, args, "device", "device"), ) if err != nil { return err diff --git a/pkg/cmd/devices/getsupportedseries/getSupportedSeries.auto.go b/pkg/cmd/devices/getsupportedseries/getSupportedSeries.auto.go index 206f1e470..6f0fd99ba 100644 --- a/pkg/cmd/devices/getsupportedseries/getSupportedSeries.auto.go +++ b/pkg/cmd/devices/getsupportedseries/getSupportedSeries.auto.go @@ -153,7 +153,7 @@ func (n *GetSupportedSeriesCmd) RunE(cmd *cobra.Command, args []string) error { cmd, path, inputIterators, - c8yfetcher.WithDeviceByNameFirstMatch(client, args, "device", "device"), + c8yfetcher.WithDeviceByNameFirstMatch(n.factory, args, "device", "device"), ) if err != nil { return err diff --git a/pkg/cmd/devices/list/list.auto.go b/pkg/cmd/devices/list/list.auto.go index 34770a914..ee646024c 100644 --- a/pkg/cmd/devices/list/list.auto.go +++ b/pkg/cmd/devices/list/list.auto.go @@ -150,7 +150,7 @@ func (n *ListCmd) RunE(cmd *cobra.Command, args []string) error { flags.WithEncodedRelativeTimestamp("lastMessageDateFrom", "lastMessageDateFrom", "(c8y_Availability.lastMessage ge '%s')"), flags.WithEncodedRelativeTimestamp("creationTimeDateTo", "creationTimeDateTo", "(creationTime.date le '%s')"), flags.WithEncodedRelativeTimestamp("creationTimeDateFrom", "creationTimeDateFrom", "(creationTime.date ge '%s')"), - c8yfetcher.WithDeviceGroupByNameFirstMatch(client, args, "group", "group", "bygroupid(%s)"), + c8yfetcher.WithDeviceGroupByNameFirstMatch(n.factory, args, "group", "group", "bygroupid(%s)"), }, "q", ), diff --git a/pkg/cmd/devices/listassets/listAssets.auto.go b/pkg/cmd/devices/listassets/listAssets.auto.go index 6524d06ab..7a52983c4 100644 --- a/pkg/cmd/devices/listassets/listAssets.auto.go +++ b/pkg/cmd/devices/listassets/listAssets.auto.go @@ -155,7 +155,7 @@ func (n *ListAssetsCmd) RunE(cmd *cobra.Command, args []string) error { cmd, path, inputIterators, - c8yfetcher.WithDeviceByNameFirstMatch(client, args, "id", "id"), + c8yfetcher.WithDeviceByNameFirstMatch(n.factory, args, "id", "id"), ) if err != nil { return err diff --git a/pkg/cmd/devices/listchildren/listChildren.auto.go b/pkg/cmd/devices/listchildren/listChildren.auto.go index 680987c2e..76208e5fc 100644 --- a/pkg/cmd/devices/listchildren/listChildren.auto.go +++ b/pkg/cmd/devices/listchildren/listChildren.auto.go @@ -156,7 +156,7 @@ func (n *ListChildrenCmd) RunE(cmd *cobra.Command, args []string) error { cmd, path, inputIterators, - c8yfetcher.WithDeviceByNameFirstMatch(client, args, "device", "device"), + c8yfetcher.WithDeviceByNameFirstMatch(n.factory, args, "device", "device"), flags.WithBoolValue("withChildren", "withChildren", ""), ) if err != nil { diff --git a/pkg/cmd/devices/services/create/create.auto.go b/pkg/cmd/devices/services/create/create.auto.go index 1ca63777f..88eaaaa4c 100644 --- a/pkg/cmd/devices/services/create/create.auto.go +++ b/pkg/cmd/devices/services/create/create.auto.go @@ -147,7 +147,7 @@ func (n *CreateCmd) RunE(cmd *cobra.Command, args []string) error { flags.WithStringValue("serviceType", "serviceType"), flags.WithStringValue("status", "status"), flags.WithStaticStringValue("type", "c8y_Service"), - cmdutil.WithTemplateValue(cfg), + cmdutil.WithTemplateValue(n.factory), flags.WithTemplateVariablesValue(), flags.WithRequiredProperties("name", "status", "type", "serviceType"), ) @@ -161,7 +161,7 @@ func (n *CreateCmd) RunE(cmd *cobra.Command, args []string) error { cmd, path, inputIterators, - c8yfetcher.WithDeviceByNameFirstMatch(client, args, "device", "id"), + c8yfetcher.WithDeviceByNameFirstMatch(n.factory, args, "device", "id"), ) if err != nil { return err diff --git a/pkg/cmd/devices/services/delete/delete.auto.go b/pkg/cmd/devices/services/delete/delete.auto.go index 2f4cc8af5..45a88bdea 100644 --- a/pkg/cmd/devices/services/delete/delete.auto.go +++ b/pkg/cmd/devices/services/delete/delete.auto.go @@ -155,8 +155,8 @@ func (n *DeleteCmd) RunE(cmd *cobra.Command, args []string) error { cmd, path, inputIterators, - c8yfetcher.WithDeviceByNameFirstMatch(client, args, "device", "device"), - c8yfetcher.WithDeviceServiceByNameFirstMatch(client, args, "id", "id"), + c8yfetcher.WithDeviceByNameFirstMatch(n.factory, args, "device", "device"), + c8yfetcher.WithDeviceServiceByNameFirstMatch(n.factory, "device", args, "id", "id"), ) if err != nil { return err diff --git a/pkg/cmd/devices/services/get/get.auto.go b/pkg/cmd/devices/services/get/get.auto.go index 6f56ca1c3..7fd2518d8 100644 --- a/pkg/cmd/devices/services/get/get.auto.go +++ b/pkg/cmd/devices/services/get/get.auto.go @@ -169,8 +169,8 @@ func (n *GetCmd) RunE(cmd *cobra.Command, args []string) error { cmd, path, inputIterators, - c8yfetcher.WithDeviceByNameFirstMatch(client, args, "device", "device"), - c8yfetcher.WithDeviceServiceByNameFirstMatch(client, args, "id", "id"), + c8yfetcher.WithDeviceByNameFirstMatch(n.factory, args, "device", "device"), + c8yfetcher.WithDeviceServiceByNameFirstMatch(n.factory, "device", args, "id", "id"), ) if err != nil { return err diff --git a/pkg/cmd/devices/services/list/list.auto.go b/pkg/cmd/devices/services/list/list.auto.go index e9fb59493..4fa5d8745 100644 --- a/pkg/cmd/devices/services/list/list.auto.go +++ b/pkg/cmd/devices/services/list/list.auto.go @@ -175,7 +175,7 @@ func (n *ListCmd) RunE(cmd *cobra.Command, args []string) error { cmd, path, inputIterators, - c8yfetcher.WithDeviceByNameFirstMatch(client, args, "device", "device"), + c8yfetcher.WithDeviceByNameFirstMatch(n.factory, args, "device", "device"), ) if err != nil { return err diff --git a/pkg/cmd/devices/services/update/update.auto.go b/pkg/cmd/devices/services/update/update.auto.go index 6a12d4ded..2cd872d52 100644 --- a/pkg/cmd/devices/services/update/update.auto.go +++ b/pkg/cmd/devices/services/update/update.auto.go @@ -150,7 +150,7 @@ func (n *UpdateCmd) RunE(cmd *cobra.Command, args []string) error { flags.WithStringValue("name", "name"), flags.WithStringValue("serviceType", "serviceType"), flags.WithStringValue("status", "status"), - cmdutil.WithTemplateValue(cfg), + cmdutil.WithTemplateValue(n.factory), flags.WithTemplateVariablesValue(), ) if err != nil { @@ -163,8 +163,8 @@ func (n *UpdateCmd) RunE(cmd *cobra.Command, args []string) error { cmd, path, inputIterators, - c8yfetcher.WithDeviceByNameFirstMatch(client, args, "device", "device"), - c8yfetcher.WithDeviceServiceByNameFirstMatch(client, args, "id", "id"), + c8yfetcher.WithDeviceByNameFirstMatch(n.factory, args, "device", "device"), + c8yfetcher.WithDeviceServiceByNameFirstMatch(n.factory, "device", args, "id", "id"), ) if err != nil { return err diff --git a/pkg/cmd/devices/setrequiredavailability/setRequiredAvailability.auto.go b/pkg/cmd/devices/setrequiredavailability/setRequiredAvailability.auto.go index 66b56d79f..462c28163 100644 --- a/pkg/cmd/devices/setrequiredavailability/setRequiredAvailability.auto.go +++ b/pkg/cmd/devices/setrequiredavailability/setRequiredAvailability.auto.go @@ -133,7 +133,7 @@ func (n *SetRequiredAvailabilityCmd) RunE(cmd *cobra.Command, args []string) err inputIterators, flags.WithDataFlagValue(), flags.WithIntValue("interval", "c8y_RequiredAvailability.responseInterval"), - cmdutil.WithTemplateValue(cfg), + cmdutil.WithTemplateValue(n.factory), flags.WithTemplateVariablesValue(), ) if err != nil { diff --git a/pkg/cmd/devices/statistics/list/list.auto.go b/pkg/cmd/devices/statistics/list/list.auto.go index 737d89472..f0dd7166f 100644 --- a/pkg/cmd/devices/statistics/list/list.auto.go +++ b/pkg/cmd/devices/statistics/list/list.auto.go @@ -113,7 +113,7 @@ func (n *ListCmd) RunE(cmd *cobra.Command, args []string) error { query, inputIterators, flags.WithCustomStringSlice(func() ([]string, error) { return cfg.GetQueryParameters(), nil }, "custom"), - c8yfetcher.WithDeviceByNameFirstMatch(client, args, "device", "deviceId"), + c8yfetcher.WithDeviceByNameFirstMatch(n.factory, args, "device", "deviceId"), ) if err != nil { return cmderrors.NewUserError(err) @@ -172,7 +172,7 @@ func (n *ListCmd) RunE(cmd *cobra.Command, args []string) error { inputIterators, flags.WithRelativeDate(false, "date", "date"), flags.WithStringValue("type", "type"), - flags.WithStringDefaultValue(client.TenantName, "tenant", "tenant"), + flags.WithStringDefaultValue(n.factory.GetTenant(), "tenant", "tenant"), ) if err != nil { return err diff --git a/pkg/cmd/devices/unassignchild/unassignChild.auto.go b/pkg/cmd/devices/unassignchild/unassignChild.auto.go index ee95d00de..a54f38956 100644 --- a/pkg/cmd/devices/unassignchild/unassignChild.auto.go +++ b/pkg/cmd/devices/unassignchild/unassignChild.auto.go @@ -154,8 +154,8 @@ func (n *UnassignChildCmd) RunE(cmd *cobra.Command, args []string) error { cmd, path, inputIterators, - c8yfetcher.WithDeviceByNameFirstMatch(client, args, "device", "device"), - c8yfetcher.WithDeviceByNameFirstMatch(client, args, "childDevice", "childDevice"), + c8yfetcher.WithDeviceByNameFirstMatch(n.factory, args, "device", "device"), + c8yfetcher.WithDeviceByNameFirstMatch(n.factory, args, "childDevice", "childDevice"), ) if err != nil { return err diff --git a/pkg/cmd/devices/update/update.auto.go b/pkg/cmd/devices/update/update.auto.go index 07b90f581..dfde26817 100644 --- a/pkg/cmd/devices/update/update.auto.go +++ b/pkg/cmd/devices/update/update.auto.go @@ -148,7 +148,7 @@ func (n *UpdateCmd) RunE(cmd *cobra.Command, args []string) error { inputIterators, flags.WithDataFlagValue(), flags.WithStringValue("newName", "name"), - cmdutil.WithTemplateValue(cfg), + cmdutil.WithTemplateValue(n.factory), flags.WithTemplateVariablesValue(), ) if err != nil { @@ -161,7 +161,7 @@ func (n *UpdateCmd) RunE(cmd *cobra.Command, args []string) error { cmd, path, inputIterators, - c8yfetcher.WithDeviceByNameFirstMatch(client, args, "id", "id"), + c8yfetcher.WithDeviceByNameFirstMatch(n.factory, args, "id", "id"), ) if err != nil { return err diff --git a/pkg/cmd/devices/user/get/get.auto.go b/pkg/cmd/devices/user/get/get.auto.go index 72122849e..5c10a160e 100644 --- a/pkg/cmd/devices/user/get/get.auto.go +++ b/pkg/cmd/devices/user/get/get.auto.go @@ -153,7 +153,7 @@ func (n *GetCmd) RunE(cmd *cobra.Command, args []string) error { cmd, path, inputIterators, - c8yfetcher.WithDeviceByNameFirstMatch(client, args, "id", "id"), + c8yfetcher.WithDeviceByNameFirstMatch(n.factory, args, "id", "id"), ) if err != nil { return err diff --git a/pkg/cmd/devices/user/update/update.auto.go b/pkg/cmd/devices/user/update/update.auto.go index 0d61c7459..6187af467 100644 --- a/pkg/cmd/devices/user/update/update.auto.go +++ b/pkg/cmd/devices/user/update/update.auto.go @@ -141,7 +141,7 @@ func (n *UpdateCmd) RunE(cmd *cobra.Command, args []string) error { inputIterators, flags.WithDataFlagValue(), flags.WithBoolValue("enabled", "enabled", ""), - cmdutil.WithTemplateValue(cfg), + cmdutil.WithTemplateValue(n.factory), flags.WithTemplateVariablesValue(), flags.WithRequiredProperties("enabled"), ) @@ -155,7 +155,7 @@ func (n *UpdateCmd) RunE(cmd *cobra.Command, args []string) error { cmd, path, inputIterators, - c8yfetcher.WithDeviceByNameFirstMatch(client, args, "id", "id"), + c8yfetcher.WithDeviceByNameFirstMatch(n.factory, args, "id", "id"), ) if err != nil { return err diff --git a/pkg/cmd/events/create/create.auto.go b/pkg/cmd/events/create/create.auto.go index f6225c61b..867c3649d 100644 --- a/pkg/cmd/events/create/create.auto.go +++ b/pkg/cmd/events/create/create.auto.go @@ -144,13 +144,13 @@ func (n *CreateCmd) RunE(cmd *cobra.Command, args []string) error { body, inputIterators, flags.WithDataFlagValue(), - c8yfetcher.WithDeviceByNameFirstMatch(client, args, "device", "source.id"), + c8yfetcher.WithDeviceByNameFirstMatch(n.factory, args, "device", "source.id"), flags.WithRelativeTimestamp("time", "time"), flags.WithStringValue("type", "type"), flags.WithStringValue("text", "text"), flags.WithDefaultTemplateString(` {time: _.Now('0s')}`), - cmdutil.WithTemplateValue(cfg), + cmdutil.WithTemplateValue(n.factory), flags.WithTemplateVariablesValue(), flags.WithRequiredProperties("type", "text", "time", "source.id"), ) diff --git a/pkg/cmd/events/createbinary/createBinary.auto.go b/pkg/cmd/events/createbinary/createBinary.auto.go index dd6b01868..b19aaab10 100644 --- a/pkg/cmd/events/createbinary/createBinary.auto.go +++ b/pkg/cmd/events/createbinary/createBinary.auto.go @@ -143,7 +143,7 @@ func (n *CreateBinaryCmd) RunE(cmd *cobra.Command, args []string) error { inputIterators, flags.WithDataFlagValue(), flags.WithStringValue("name", "name"), - cmdutil.WithTemplateValue(cfg), + cmdutil.WithTemplateValue(n.factory), flags.WithTemplateVariablesValue(), ) if err != nil { diff --git a/pkg/cmd/events/deletecollection/deleteCollection.auto.go b/pkg/cmd/events/deletecollection/deleteCollection.auto.go index 1f513c7f0..625133fb4 100644 --- a/pkg/cmd/events/deletecollection/deleteCollection.auto.go +++ b/pkg/cmd/events/deletecollection/deleteCollection.auto.go @@ -108,7 +108,7 @@ func (n *DeleteCollectionCmd) RunE(cmd *cobra.Command, args []string) error { query, inputIterators, flags.WithCustomStringSlice(func() ([]string, error) { return cfg.GetQueryParameters(), nil }, "custom"), - c8yfetcher.WithDeviceByNameFirstMatch(client, args, "device", "source"), + c8yfetcher.WithDeviceByNameFirstMatch(n.factory, args, "device", "source"), flags.WithStringValue("type", "type"), flags.WithStringValue("fragmentType", "fragmentType"), flags.WithEncodedRelativeTimestamp("createdFrom", "createdFrom"), diff --git a/pkg/cmd/events/list/list.auto.go b/pkg/cmd/events/list/list.auto.go index 124488445..46162546b 100644 --- a/pkg/cmd/events/list/list.auto.go +++ b/pkg/cmd/events/list/list.auto.go @@ -117,7 +117,7 @@ func (n *ListCmd) RunE(cmd *cobra.Command, args []string) error { query, inputIterators, flags.WithCustomStringSlice(func() ([]string, error) { return cfg.GetQueryParameters(), nil }, "custom"), - c8yfetcher.WithDeviceByNameFirstMatch(client, args, "device", "source"), + c8yfetcher.WithDeviceByNameFirstMatch(n.factory, args, "device", "source"), flags.WithStringValue("type", "type"), flags.WithStringValue("fragmentType", "fragmentType"), flags.WithStringValue("fragmentValue", "fragmentValue"), diff --git a/pkg/cmd/events/subscribe/subscribe.manual.go b/pkg/cmd/events/subscribe/subscribe.manual.go index 7dbe167bf..1f322abb5 100644 --- a/pkg/cmd/events/subscribe/subscribe.manual.go +++ b/pkg/cmd/events/subscribe/subscribe.manual.go @@ -95,7 +95,7 @@ func (n *CmdSubscribe) RunE(cmd *cobra.Command, args []string) error { path, inputIterators, flags.WithStringDefaultValue("*", "device", "device"), - c8yfetcher.WithDeviceByNameFirstMatch(client, args, "device", "device"), + c8yfetcher.WithDeviceByNameFirstMatch(n.factory, args, "device", "device"), ) if err != nil { return err diff --git a/pkg/cmd/events/update/update.auto.go b/pkg/cmd/events/update/update.auto.go index 611946e60..762feb421 100644 --- a/pkg/cmd/events/update/update.auto.go +++ b/pkg/cmd/events/update/update.auto.go @@ -140,7 +140,7 @@ func (n *UpdateCmd) RunE(cmd *cobra.Command, args []string) error { inputIterators, flags.WithDataFlagValue(), flags.WithStringValue("text", "text"), - cmdutil.WithTemplateValue(cfg), + cmdutil.WithTemplateValue(n.factory), flags.WithTemplateVariablesValue(), ) if err != nil { diff --git a/pkg/cmd/extension/alias_extension.go b/pkg/cmd/extension/alias_extension.go new file mode 100644 index 000000000..32688a4fb --- /dev/null +++ b/pkg/cmd/extension/alias_extension.go @@ -0,0 +1,28 @@ +package extension + +type AliasExtension struct { + Source string `json:"-" yaml:"-"` + Name string `json:"name,omitempty" yaml:"name,omitempty"` + Command string `json:"command,omitempty" yaml:"command,omitempty"` + Description string `json:"description,omitempty" yaml:"description,omitempty"` + Shell bool `json:"shell" yaml:"shell"` +} + +func (a *AliasExtension) GetCommand() string { + if a.Shell { + return "!" + a.Command + } + return a.Command +} + +func (a *AliasExtension) GetName() string { + return a.Name +} + +func (a *AliasExtension) GetDescription() string { + return a.Description +} + +func (a *AliasExtension) IsShell() bool { + return a.Shell +} diff --git a/pkg/cmd/extension/command.go b/pkg/cmd/extension/command.go new file mode 100644 index 000000000..41813abc3 --- /dev/null +++ b/pkg/cmd/extension/command.go @@ -0,0 +1,525 @@ +package extension + +import ( + "encoding/json" + "errors" + "fmt" + _io "io" + "os" + "path/filepath" + "strings" + + "github.com/MakeNowJust/heredoc/v2" + "github.com/reubenmiller/go-c8y-cli/v2/internal/ghrepo" + "github.com/reubenmiller/go-c8y-cli/v2/pkg/cmderrors" + "github.com/reubenmiller/go-c8y-cli/v2/pkg/cmdutil" + "github.com/reubenmiller/go-c8y-cli/v2/pkg/completion" + "github.com/reubenmiller/go-c8y-cli/v2/pkg/extensions" + "github.com/reubenmiller/go-c8y-cli/v2/pkg/flags" + "github.com/reubenmiller/go-c8y-cli/v2/pkg/git" + "github.com/reubenmiller/go-c8y-cli/v2/pkg/iterator" + "github.com/reubenmiller/go-c8y-cli/v2/pkg/prompt" + "github.com/spf13/cobra" +) + +var ExtPrefix = "c8y-" + +func NewCmdExtension(f *cmdutil.Factory) *cobra.Command { + m := f.ExtensionManager + io := f.IOStreams + prompter := prompt.NewPrompt(nil) + + extCmd := cobra.Command{ + Use: "extensions", + Short: "Manage c8y extensions", + Long: heredoc.Docf(` + go-c8y-cli extensions are repositories that provide additional c8y commands. + + The name of the extension repository must start with "c8y-" and it must contain an + executable of the same name. All arguments passed to the %[1]sc8y %[1]s invocation + will be forwarded to the %[1]sc8y-%[1]s executable of the extension. + + An extension cannot override any of the core c8y commands. + + See the list of available extensions at . + `, "`"), + Aliases: []string{"extension"}, + } + + extCmd.AddCommand( + &cobra.Command{ + Use: "list", + Short: "List installed extension commands", + Aliases: []string{"ls"}, + Args: cobra.NoArgs, + RunE: func(cmd *cobra.Command, args []string) error { + cmds := m().List() + if len(cmds) == 0 { + return cmderrors.NewSystemError("no installed extensions found") + } + cfg, err := f.Config() + + if err != nil { + return err + } + + for _, c := range cmds { + ext := map[string]interface{}{} + var repo string + if u, err := git.ParseURL(c.URL()); err == nil { + repo = u.String() + if r, err := ghrepo.FromURL(u); err == nil { + repo = ghrepo.FullName(r) + } + } + + version := displayExtensionVersion(c, c.CurrentVersion()) + + ext["name"] = c.Name() + ext["repo"] = repo + ext["version"] = version + ext["pinned"] = c.IsPinned() + ext["path"] = c.Path() + ext["isLocal"] = c.IsLocal() + ext["isBinary"] = c.IsBinary() + + rowText, err := json.Marshal(ext) + if err != nil { + return cmderrors.NewUserError("Settings error. ", err) + } + + f.WriteJSONToConsole(cfg, cmd, "", rowText) + } + return nil + }, + }, + func() *cobra.Command { + var pinFlag string + var nameFlag string + cmd := &cobra.Command{ + Use: "install ", + Short: "Install a c8y extensions from a repository", + Long: heredoc.Doc(` + Install a GitHub repository locally as a Cumulocity CLI extension. + + The repository argument can be specified in "owner/repo" format as well as a full URL. + The URL format is useful when the repository is not hosted on github.com. + + To install an extension in development from the current directory, use "." as the + value of the repository argument. + + See the list of available extensions at . + `), + Example: heredoc.Doc(` + $ c8y extensions install owner/c8y-extension + $ c8y extensions install https://git.example.com/owner/c8y-extension + $ c8y extensions install . + `), + Args: func(cmd *cobra.Command, args []string) error { + err := cobra.MinimumNArgs(1)(cmd, args) + if err != nil { + return fmt.Errorf("%w. must specify a repository to install from", err) + } + return err + }, + RunE: func(cmd *cobra.Command, args []string) error { + cfg, err := f.Config() + if err != nil { + return err + } + if fileInfo, err := os.Stat(args[0]); err == nil && fileInfo.IsDir() { + if pinFlag != "" { + return fmt.Errorf("local extensions cannot be pinned") + } + + sourcePath, err := filepath.Abs(args[0]) + if err != nil { + return err + } + + if err := m().InstallLocal(sourcePath, nameFlag); err != nil { + return err + } + if io.IsStdoutTTY() { + successStr := "Installed" + if cfg.DryRun() { + successStr = "Would have installed" + } + cs := io.ColorScheme() + fmt.Fprintf(io.Out, "%s %s extension %s\n", cs.SuccessIcon(), successStr, filepath.Base(sourcePath)) + } + return nil + } + + repo, err := ghrepo.FromFullName(args[0]) + if err != nil { + return err + } + + extName := nameFlag + skipNameCheck := true + if extName == "" { + extName = repo.RepoName() + skipNameCheck = false + } else { + if !strings.HasPrefix(extName, ExtPrefix) { + extName = ExtPrefix + extName + } + } + if err := checkValidExtension(cmd.Root(), m(), extName, skipNameCheck); err != nil { + return err + } + + cs := io.ColorScheme() + if err := m().Install(repo, extName, pinFlag); err != nil { + if errors.Is(err, ErrReleaseNotFound) { + return fmt.Errorf("%s Could not find a release of %s for %s", + cs.FailureIcon(), args[0], cs.Cyan(pinFlag)) + } else if errors.Is(err, ErrCommitNotFound) { + return fmt.Errorf("%s %s does not exist in %s", + cs.FailureIcon(), cs.Cyan(pinFlag), args[0]) + } else if errors.Is(err, ErrRepositoryNotFound) { + return fmt.Errorf("%s Could not find extension '%s' on host %s", + cs.FailureIcon(), args[0], repo.RepoHost()) + } + return err + } + + if io.IsStdoutTTY() { + fmt.Fprintf(io.Out, "%s Installed extension %s\n", cs.SuccessIcon(), extName) + if pinFlag != "" { + fmt.Fprintf(io.Out, "%s Pinned extension at %s\n", cs.SuccessIcon(), cs.Cyan(pinFlag)) + } + } + return nil + }, + } + cmd.Flags().StringVar(&pinFlag, "pin", "", "pin extension to a release tag or commit ref") + cmd.Flags().StringVar(&nameFlag, "name", "", "use custom name for the extension") + completion.WithOptions( + cmd, + completion.MarkLocalFlag(), + ) + return cmd + }(), + func() *cobra.Command { + var flagAll bool + cmd := &cobra.Command{ + Use: "update { | --all}", + Short: "Update installed extensions", + ValidArgsFunction: func(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) { + cmds := m().List() + names := []string{} + for _, c := range cmds { + names = append(names, c.Name()) + } + return names, cobra.ShellCompDirectiveNoFileComp + }, + Args: func(cmd *cobra.Command, args []string) error { + if len(args) == 0 && !flagAll { + return cmderrors.NewUserError("specify an extension to update or `--all`") + } + if len(args) > 0 && flagAll { + return cmderrors.NewUserError("cannot use `--all` with extension name") + } + if len(args) > 1 { + return cmderrors.NewUserError("too many arguments") + } + return nil + }, + RunE: func(cmd *cobra.Command, args []string) error { + var name string + if len(args) > 0 { + name = normalizeExtensionSelector(args[0]) + } + cfg, cfgErr := f.Config() + if cfgErr != nil { + return cfgErr + } + if cfg.DryRun() { + m().EnableDryRunMode() + } + cs := io.ColorScheme() + err := m().Upgrade(name, cfg.Force()) + if err != nil && !errors.Is(err, ErrUpToDate) { + if name != "" { + fmt.Fprintf(io.ErrOut, "%s Failed updating extension %s: %s\n", cs.FailureIcon(), name, err) + } else if errors.Is(err, ErrNoExtensionsInstalled) { + return cmderrors.NewSystemError("no installed extensions found") + } else { + fmt.Fprintf(io.ErrOut, "%s Failed updating extensions\n", cs.FailureIcon()) + } + return cmderrors.NewSilentError() + } + if io.IsStdoutTTY() { + successStr := "Successfully" + + if cfg.DryRun() { + successStr = "Would have" + } + if errors.Is(err, ErrUpToDate) { + fmt.Fprintf(io.Out, "%s Extension already up to date\n", cs.SuccessIcon()) + } else if name != "" { + fmt.Fprintf(io.Out, "%s %s updated extension %s\n", cs.SuccessIcon(), successStr, name) + } else { + fmt.Fprintf(io.Out, "%s %s updated extensions\n", cs.SuccessIcon(), successStr) + } + } + return nil + }, + } + cmd.Flags().BoolVar(&flagAll, "all", false, "Update all extensions") + completion.WithOptions( + cmd, + completion.MarkLocalFlag(), + ) + return cmd + }(), + &cobra.Command{ + Use: "delete ", + Short: "Remove an installed extension", + ValidArgsFunction: func(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) { + cmds := m().List() + names := []string{} + for _, c := range cmds { + names = append(names, c.Name()) + } + return names, cobra.ShellCompDirectiveNoFileComp + }, + RunE: func(cmd *cobra.Command, args []string) error { + cfg, cfgErr := f.Config() + if cfgErr != nil { + return cfgErr + } + if cfg.DryRun() { + m().EnableDryRunMode() + } + + inputIterators, err := cmdutil.NewRequestInputIterators(cmd, cfg) + if err != nil { + return err + } + var iter iterator.Iterator + _, input, err := flags.WithPipelineIterator(&flags.PipelineOptions{ + Name: "name", + Aliases: []string{"name"}, + Disabled: inputIterators.PipeOptions.Disabled, + Values: args, // manual values + Required: false, + })(cmd, inputIterators) + + if err != nil { + return &flags.ParameterError{ + Name: "Name", + Err: fmt.Errorf("missing required argument or pipeline input. %w", flags.ErrParameterMissing), + } + } + + extName := "" + switch v := input.(type) { + case iterator.Iterator: + iter = v + default: + return fmt.Errorf("unknown iterator type") + } + + bounded := iter.IsBound() + errors := []error{} + for { + responseText, _, err := iter.GetNext() + if err != nil { + if err == _io.EOF { + break + } + return err + } + + extName = normalizeExtensionSelector(string(responseText)) + + if err := m().Remove(extName); err != nil { + errors = append(errors, err) + continue + } + + if io.IsStdoutTTY() { + successStr := "Removed" + if cfg.DryRun() { + successStr = "Would have removed" + } + cs := io.ColorScheme() + fmt.Fprintf(io.Out, "%s %s extension %s\n", cs.SuccessIconWithColor(cs.Red), successStr, extName) + } + + if !bounded { + break + } + } + + switch count := len(errors); count { + case 0: + return nil + case 1: + return errors[0] + default: + errorMessages := []string{} + for _, m := range errors { + errorMessages = append(errorMessages, m.Error()) + } + return fmt.Errorf("failed to remove %d extension/s. errors=%v", count, strings.Join(errorMessages, ", ")) + } + }, + }, + func() *cobra.Command { + promptCreate := func() (string, extensions.ExtTemplateType, error) { + extName, err := prompter.Input("Extension name", "", true, false) + if err != nil { + return extName, -1, err + } + return extName, extensions.ExtTemplateType(extensions.GitTemplateType), err + } + var flagType string + cmd := &cobra.Command{ + Use: "create []", + Short: "Create a new extension", + Example: heredoc.Doc(` + # Use interactively + c8y extensions create + + # Create a script-based extension + c8y extensions create foobar + `), + Args: cobra.MaximumNArgs(1), + RunE: func(cmd *cobra.Command, args []string) error { + if cmd.Flags().Changed("precompiled") { + if flagType != "go" && flagType != "other" { + return cmderrors.NewUserError("value for --precompiled must be 'go' or 'other'. Got '%s'", flagType) + } + } + var extName string + var err error + tmplType := extensions.GitTemplateType + if len(args) == 0 { + if io.IsStdoutTTY() { + extName, tmplType, err = promptCreate() + if err != nil { + return fmt.Errorf("could not prompt: %w", err) + } + } + } else { + extName = args[0] + if flagType == "go" { + tmplType = extensions.GoBinTemplateType + } else if flagType == "other" { + tmplType = extensions.OtherBinTemplateType + } + } + + var fullName string + + if strings.HasPrefix(extName, ExtPrefix) { + fullName = extName + extName = extName[len(ExtPrefix):] + } else { + fullName = ExtPrefix + extName + } + if err := m().Create(fullName, tmplType); err != nil { + return err + } + if !io.IsStdoutTTY() { + return nil + } + + var goBinChecks string + + steps := fmt.Sprintf( + "- run 'cd %[1]s; c8y extensions install .; c8y %[2]s --help' to see your new extension in action", + fullName, extName) + + cs := io.ColorScheme() + if tmplType == extensions.GoBinTemplateType { + goBinChecks = heredoc.Docf(` + %[1]s Downloaded Go dependencies + %[1]s Built %[2]s binary + `, cs.SuccessIcon(), fullName) + steps = heredoc.Docf(` + - run 'cd %[1]s; c8y extensions install .; c8y %[2]s' to see your new extension in action + - use 'go build && c8y %[2]s' to see changes in your code as you develop`, fullName, extName) + } else if tmplType == extensions.OtherBinTemplateType { + steps = heredoc.Docf(` + - run 'cd %[1]s; c8y extensions install .' to install your extension locally + - fill in script/build.sh with your compilation script for automated builds + - compile a %[1]s binary locally and run 'c8y %[2]s' to see changes`, fullName, extName) + } + link := "https://docs.github.com/github-cli/github-cli/creating-github-cli-extensions" + out := heredoc.Docf(` + %[1]s Created directory %[2]s + %[1]s Initialized git repository + %[1]s Set up extension scaffolding + %[6]s + %[2]s is ready for development! + + %[4]s + %[5]s + - commit and push your code to a git repository to share your extension with others. + To share on github you can use the 'gh' command: 'gh repo create' + + For more information on writing extensions: + %[3]s + `, cs.SuccessIcon(), fullName, link, cs.Bold("Next Steps"), steps, goBinChecks) + fmt.Fprint(io.Out, out) + return nil + }, + } + // FUTURE: Support other types of extensions in the future + // cmd.Flags().StringVar(&flagType, "precompiled", "", "Create a precompiled extension. Possible values: go, other") + completion.WithOptions( + cmd, + completion.MarkLocalFlag(), + ) + return cmd + }(), + ) + + for _, cmd := range extCmd.Commands() { + cmdutil.DisableAuthCheck(cmd) + } + return &extCmd +} + +func checkValidExtension(rootCmd *cobra.Command, m extensions.ExtensionManager, extName string, skipNameCheck bool) error { + // Allow prefix anyway in the extension name + if !skipNameCheck { + if !strings.HasPrefix(extName, ExtPrefix) && !strings.Contains(extName, ExtPrefix) { + return fmt.Errorf("extension repository name must start with `%s`", ExtPrefix) + } + } + + commandName := strings.TrimPrefix(extName, ExtPrefix) + if c, _, err := rootCmd.Traverse([]string{commandName}); err != nil { + return err + } else if c != rootCmd { + return fmt.Errorf("%q matches the name of a built-in command", commandName) + } + + for _, ext := range m.List() { + if ext.Name() == commandName { + return fmt.Errorf("there is already an installed extension that provides the %q command", commandName) + } + } + + return nil +} + +func normalizeExtensionSelector(n string) string { + if idx := strings.IndexRune(n, '/'); idx >= 0 { + n = n[idx+1:] + } + return strings.TrimPrefix(n, ExtPrefix) +} + +func displayExtensionVersion(ext extensions.Extension, version string) string { + if !ext.IsBinary() && len(version) > 8 { + return version[:8] + } + return version +} diff --git a/pkg/cmd/extension/command_extension.go b/pkg/cmd/extension/command_extension.go new file mode 100644 index 000000000..1147b5fe7 --- /dev/null +++ b/pkg/cmd/extension/command_extension.go @@ -0,0 +1,19 @@ +package extension + +type Command struct { + name string + command string + description string +} + +func (c *Command) Command() string { + return c.command +} + +func (c *Command) Name() string { + return c.name +} + +func (c *Command) Description() string { + return c.description +} diff --git a/pkg/cmd/extension/ext_tmpls/README.md b/pkg/cmd/extension/ext_tmpls/README.md new file mode 100644 index 000000000..4a57c7fd6 --- /dev/null +++ b/pkg/cmd/extension/ext_tmpls/README.md @@ -0,0 +1,25 @@ +# %[1]s + +go-c8y-cli extension + +## What is included? + +**Note** + +Use ✅ or 🔲 indicates if the extension includes the given functionality or not. + + +|Type|Included|Notes| +|----|:-:|-----| +|Aliases|✅|Some useful default command like `lookup `| +|Commands|✅|Commands to manage the custom inventory managed object entities used in our IoT solution| +|Templates|✅|Some random data templates and common operations| +|Views|✅|Custom device and event views| + +## Install + +The extension can be installed using the following command. + +```sh +c8y extension install owner/%[1]s +``` diff --git a/pkg/cmd/extension/ext_tmpls/apiCommandTemplate.yaml b/pkg/cmd/extension/ext_tmpls/apiCommandTemplate.yaml new file mode 100644 index 000000000..136d92e76 --- /dev/null +++ b/pkg/cmd/extension/ext_tmpls/apiCommandTemplate.yaml @@ -0,0 +1,144 @@ +# yaml-language-server: $schema=https://raw.githubusercontent.com/reubenmiller/go-c8y-cli/feat/extensions-manager/tools/schema/extensionCommands.json +--- +group: + name: devices + description: Manage devices + +# Use can use yaml anchors to reduce the amount of boilerplate +x-types-device: &type-device + type: string + description: Device. It support a custom completion/lookup using other c8y commands + pipeline: true + completion: + type: external + command: + - c8y + - devices + - list + - --query + - "name eq '%s*'" + - --select=name + lookup: + type: external + command: + - c8y + - devices + - list + - --query + - "name eq '%s*'" + - --select=id + +commands: + - name: list + description: Simple list command + descriptionLong: | + Some more detailed instructions on how to use the command + + It can also use come additional context. This command uses a the 'query-inventory' preset + so that you don't have to define all of the other flags yourself. + exampleList: + - command: c8y %[1]s devices list + description: List devices + preset: + type: query-inventory + options: + param: q + value: has(c8y_IsLinux) + extensions: + - name: model + type: string + format: c8y_Hardware.model eq '%s' + description: Filter by model + + - name: excludeAgents + type: booleanDefault + description: Exclude agents + value: 'not(has(com_cumulocity_model_Agent))' + + - name: get + description: Get device + descriptionLong: Get a device using a simple GET request + method: GET + path: inventory/managedObjects/{id} + exampleList: + - command: c8y %[1]s devices get --id 1234 + description: Get a device by id + - command: c8y %[1]s devices get --id linux + description: Get a device by name + pathParameters: + - name: id + <<: *type-device + + - name: update + description: Update object + descriptionLong: Update + method: PUT + path: inventory/managedObjects/{id} + pathParameters: + - name: id + <<: *type-device + body: + - name: name + type: string + description: Profile name + required: true + + - name: delete + description: Delete device + method: DELETE + path: inventory/managedObjects/{id} + pathParameters: + - name: id + <<: *type-device + + - name: create + description: Create device + descriptionLong: | + Create a device by building the body via commands or the template. + method: POST + path: inventory/managedObjects + exampleList: + - command: | + c8y %[1]s create --name "linux device 001" --template "{one: 1}" + description: Create a new managed object using a template + + - command: c8y %[1]s create --template %[1]s::device.jsonnet + description: Create a device using the template provided by the extension + body: + - name: name + type: string + description: Name + pipeline: true + + - name: time + type: datetime + description: Timestamp (ISO-8601 format). Supports fixed or relative time + + - name: day + type: date + description: Date (without time, e.g. 2023-04-23). Supports fixed or relative time + + - name: subType + type: string + description: Sub type with a static list of suggested values + validationSet: + - example1 + - example2 + + bodyTemplates: + # Apply a static template to enforce specific fragments + - type: jsonnet + template: "{com_cumulocity_model_Agent:{}, c8y_IsDevice:{}, c8y_IsLinux:{}}" + + - name: hidden + description: Hidden command + descriptionLong: | + Hidden commands don't appear in the list of commands, however you can still + use them if you know the name. This is great for keeping backwards compatibility + when renaming commands. + exampleList: + - command: c8y oee features hidden --help + description: Show help for the hidden command + hidden: true + path: something/secret + method: POST diff --git a/pkg/cmd/extension/ext_tmpls/customCommand.jsonnet b/pkg/cmd/extension/ext_tmpls/customCommand.jsonnet new file mode 100644 index 000000000..50aee2f02 --- /dev/null +++ b/pkg/cmd/extension/ext_tmpls/customCommand.jsonnet @@ -0,0 +1,8 @@ +// Description: Create custom operation +{ + deviceId: "1234", // Dummy value (this will be overwritten when using with c8y operations create) + description: "Executing custom operation: " + $.com_CustomOperation.param1, + com_CustomOperation: { + param1: var("param1", "do_something"), + } +} \ No newline at end of file diff --git a/pkg/cmd/extension/ext_tmpls/device.jsonnet b/pkg/cmd/extension/ext_tmpls/device.jsonnet new file mode 100644 index 000000000..f7d8a20a1 --- /dev/null +++ b/pkg/cmd/extension/ext_tmpls/device.jsonnet @@ -0,0 +1,7 @@ +// Description: Custom device +{ + c8y_IsLinux: {}, + c8y_Hardware: { + model: "linux" + } +} \ No newline at end of file diff --git a/pkg/cmd/extension/ext_tmpls/exampleDevice.json b/pkg/cmd/extension/ext_tmpls/exampleDevice.json new file mode 100644 index 000000000..1b250ec2b --- /dev/null +++ b/pkg/cmd/extension/ext_tmpls/exampleDevice.json @@ -0,0 +1,19 @@ +{ + "$schema": "https://raw.githubusercontent.com/reubenmiller/go-c8y-cli/feat/extensions-manager/tools/schema/views.json", + "version": "v1", + "definitions": [ + { + "name": "customDevice", + "priority": 350, + "fragments": ["c8y_IsDevice"], + "columns": [ + "id", + "name", + "type", + "customProperty", + "connAlias:c8y_Availability.status", + "c8y_Connection.status" + ] + } + ] +} \ No newline at end of file diff --git a/pkg/cmd/extension/ext_tmpls/extension.yaml b/pkg/cmd/extension/ext_tmpls/extension.yaml new file mode 100644 index 000000000..ee7ae86fd --- /dev/null +++ b/pkg/cmd/extension/ext_tmpls/extension.yaml @@ -0,0 +1,14 @@ +# yaml-language-server: $schema=https://raw.githubusercontent.com/reubenmiller/go-c8y-cli/feat/extensions-manager/tools/schema/extension.json +version: "v1" +aliases: + - name: lookup + description: Lookup external identity by name + command: | + identity get --name "$1" + shell: false + + - name: customCommand + description: Example description + command: | + c8y devices list --type "myType" + shell: true diff --git a/pkg/cmd/extension/ext_tmpls/script-inventory-example.sh b/pkg/cmd/extension/ext_tmpls/script-inventory-example.sh new file mode 100644 index 000000000..11fca7a76 --- /dev/null +++ b/pkg/cmd/extension/ext_tmpls/script-inventory-example.sh @@ -0,0 +1,98 @@ +#!/usr/bin/env bash +set -e + +# Describe how to use your command +help() { + cat < Match by name + --label Match by c8y_Kpi.label + --onlyAgents Only include managed objects with the 'com_cumulocity_model_Agent' fragment +EOF +} + +examples () { + cat <&2 + +# Inventory query builder using +FLAGS=() +QUERY_PARTS=() +POSITIONAL_ARGS=() + +function join_by { + local d=${1-} f=${2-} + if shift 2; then + printf %%s "$f" "${@/#/$d}" + fi +} + +# # Parse options for flags with values: --flag , or boolean/switch flags: --help|-h +while [ $# -gt 0 ]; do + case "$1" in + --name) + QUERY_PARTS+=( + "name eq '$2'" + ) + shift + ;; + --label) + QUERY_PARTS+=( + "c8y_Kpi.label eq '$2'" + ) + shift + ;; + --onlyAgents) + QUERY_PARTS+=( + "has(com_cumulocity_model_Agent)" + ) + ;; + # Support showing the help when users provide '-h' or '--help' + -h|--help) + help + exit 0 + ;; + # Support showing just the examples using '--examples' + --examples) + examples + exit 0 + ;; + *) + POSITIONAL_ARGS+=("$1") + ;; + esac + shift +done + +# Restore additional arguments which can then be referenced via "$@" and "$1" etc. +set -- "${POSITIONAL_ARGS[@]}" + +if [ "${#QUERY_PARTS[@]}" -gt 0 ]; then + FLAGS+=( + --query + "$(join_by " and " "${QUERY_PARTS[@]}")" + ) +fi + +# Call another c8y command which actually does the heavy lifting +c8y inventory find "${FLAGS[@]}" "$@" diff --git a/pkg/cmd/extension/ext_tmpls/script.sh b/pkg/cmd/extension/ext_tmpls/script.sh new file mode 100644 index 000000000..c4ad7591d --- /dev/null +++ b/pkg/cmd/extension/ext_tmpls/script.sh @@ -0,0 +1,120 @@ +#!/usr/bin/env bash +set -e + +# Describe how to use your command +help() { + cat < Match by name + --onlyAgents Only include managed objects with the 'com_cumulocity_model_Agent' fragment +EOF +} + +examples () { + cat <&2 + +# Snippets to help get started: + +# Get the script's directory (useful if you need to reference some other assets provided by the extension using a relative path) +# SCRIPT_DIR=$( cd -- "$( dirname -- "${BASH_SOURCE[0]}" )" &> /dev/null && pwd ) +# cat "$SCRIPT_DIR/../templates/mytemplate.jsonnet" + +# Check for minimal positional arguments +# if [ $# -lt 1 ]; then +# echo "Missing positional arguments. This command requires at least 1 argument" +# help +# exit 1 +# fi + +# Determine if an executable is in the PATH +# if ! type -p python3 >/dev/null; then +# echo "python3 not found on the system" >&2 +# exit 1 +# fi + +# Pass arguments through to another command +# c8y inventory find "$@" +# + +# Using the c8y api command to send to a custom endpoint +# TEMPLATE=' +# { +# someNestedFragment: { +# info: "value", +# }, +# } +# ' +# exec c8y api POST "/service/my-service/endpoint" --template="${TEMPLATE}" + +# Inventory query builder using +# +# FLAGS=() +# QUERY_PARTS=() + +# function join_by { +# local d=${1-} f=${2-} +# if shift 2; then +# printf %%s "$f" "${@/#/$d}" +# fi +# } + +# # Parse options for flags with values: --flag , or boolean/switch flags: --help|-h +# while [ $# -gt 0 ]; do +# case "$1" in +# --name) +# QUERY_PARTS+=( +# "name eq '$2'" +# ) +# shift +# ;; +# --onlyAgents) +# QUERY_PARTS+=( +# "has(com_cumulocity_model_Agent)" +# ) +# ;; +# Support showing the help when users provide '-h' or '--help' +# -h|--help) +# help +# exit 0 +# ;; +# # Support showing just the examples using '--examples' +# --examples) +# examples +# exit 0 +# ;; +# *) +# REST_ARGS+=("$1") +# ;; +# esac +# shift +# done + +# # Reset additional arguments which can then be referenced via "$@" +# set -- "${REST_ARGS[@]}" + +# if [ "${#QUERY_PARTS[@]}" -gt 0 ]; then +# FLAGS+=( +# --query +# "$(join_by " and " "${QUERY_PARTS[@]}")" +# ) +# fi + +# c8y inventory find "${FLAGS[@]}" "$@" diff --git a/pkg/cmd/extension/extension.go b/pkg/cmd/extension/extension.go new file mode 100644 index 000000000..bb2354b91 --- /dev/null +++ b/pkg/cmd/extension/extension.go @@ -0,0 +1,139 @@ +package extension + +import ( + "io" + "io/fs" + "os" + "path/filepath" + "strings" + + "github.com/reubenmiller/go-c8y-cli/v2/pkg/extensions" + "gopkg.in/yaml.v3" +) + +const manifestName = "manifest.yml" +const fileAlias = "extension.yaml" +const templateName = "templates" +const viewsName = "views" +const apiName = "api" +const commandsName = "commands" + +type ExtensionKind int + +const ( + GitKind ExtensionKind = iota + BinaryKind +) + +type Extension struct { + path string + url string + isLocal bool + isPinned bool + currentVersion string + latestVersion string + kind ExtensionKind + + aliases []extensions.Alias +} + +type ExtensionFile struct { + Aliases []AliasExtension `json:"aliases,omitempty" yaml:"aliases,omitempty"` +} + +func (e *Extension) Name() string { + return strings.TrimPrefix(filepath.Base(e.path), ExtPrefix) +} + +func (e *Extension) Path() string { + return e.path +} + +func (e *Extension) URL() string { + return e.url +} + +func (e *Extension) IsLocal() bool { + return e.isLocal +} + +func (e *Extension) CurrentVersion() string { + return e.currentVersion +} + +func (e *Extension) IsPinned() bool { + return e.isPinned +} + +func (e *Extension) UpdateAvailable() bool { + if e.isPinned || + e.isLocal || + e.currentVersion == "" || + e.latestVersion == "" || + e.currentVersion == e.latestVersion { + return false + } + return true +} + +func (e *Extension) IsBinary() bool { + return e.kind == BinaryKind +} + +// Custom extension components +func (e *Extension) Aliases() ([]extensions.Alias, error) { + if len(e.aliases) > 0 { + return e.aliases, nil + } + path := filepath.Join(e.path, fileAlias) + aliases := make([]extensions.Alias, 0) + + if file, err := os.Open(path); err == nil { + if b, bErr := io.ReadAll(file); bErr == nil { + ext := &ExtensionFile{} + if jErr := yaml.Unmarshal(b, ext); jErr != nil { + return nil, jErr + } + + for i, alias := range ext.Aliases { + if alias.GetName() != "" && alias.GetCommand() != "" { + aliases = append(aliases, &ext.Aliases[i]) + } + } + } + } + e.aliases = aliases + return aliases, nil +} + +func (e *Extension) Commands() ([]extensions.Command, error) { + path := filepath.Join(e.path, commandsName) + commands := make([]extensions.Command, 0) + + err := filepath.Walk(path, func(ipath string, info fs.FileInfo, err error) error { + if err != nil { + return err + } + if !info.IsDir() && !strings.HasPrefix(info.Name(), ".") { + commands = append(commands, &Command{ + name: strings.TrimSpace(strings.ReplaceAll(strings.TrimPrefix(ipath, path), string(os.PathSeparator), " ")), + command: ipath, + }) + } + return nil + }) + + // Ignore if the folder does not exist, as commands are not mandatory + if os.IsNotExist(err) { + return commands, nil + } + return commands, err +} + +func (e *Extension) TemplatePath() string { + return filepath.Join(e.path, templateName) +} + +func (e *Extension) ViewPath() string { + return filepath.Join(e.path, viewsName) +} diff --git a/pkg/cmd/extension/extension_source.go b/pkg/cmd/extension/extension_source.go new file mode 100644 index 000000000..e3ff0abaa --- /dev/null +++ b/pkg/cmd/extension/extension_source.go @@ -0,0 +1,27 @@ +package extension + +type ExtensionSource struct { + Name string + Paths []string +} + +type ExtensionItemCollection struct { + Items []ExtensionSource +} + +func (t *ExtensionItemCollection) Add(name string, paths []string) { + if len(paths) > 0 { + t.Items = append(t.Items, ExtensionSource{name, paths}) + } +} + +func (t *ExtensionItemCollection) AddSources(sources []ExtensionSource) { + t.Items = append(t.Items, sources...) +} + +func NewExtensionItemCollection() *ExtensionItemCollection { + collection := &ExtensionItemCollection{ + Items: []ExtensionSource{}, + } + return collection +} diff --git a/pkg/cmd/extension/http.go b/pkg/cmd/extension/http.go new file mode 100644 index 000000000..cb2a1bced --- /dev/null +++ b/pkg/cmd/extension/http.go @@ -0,0 +1,227 @@ +package extension + +import ( + "encoding/json" + "errors" + "fmt" + "io" + "net/http" + "os" + "strings" + + "github.com/reubenmiller/go-c8y-cli/v2/internal/ghrepo" +) + +// localhost is the domain name of a local GitHub instance +const localhost = "github.localhost" + +func HandleHTTPError(resp *http.Response) error { + return fmt.Errorf("http error") +} + +func repoExists(httpClient *http.Client, repo ghrepo.Interface) (bool, error) { + url := fmt.Sprintf("%srepos/%s/%s", RESTPrefix(repo.RepoHost()), repo.RepoOwner(), repo.RepoName()) + req, err := http.NewRequest("GET", url, nil) + if err != nil { + return false, err + } + + resp, err := httpClient.Do(req) + if err != nil { + return false, err + } + defer resp.Body.Close() + + switch resp.StatusCode { + case 200: + return true, nil + case 404: + return false, nil + default: + return false, HandleHTTPError(resp) + } +} + +func hasBundle(httpClient *http.Client, repo ghrepo.Interface) (bool, error) { + path := fmt.Sprintf("repos/%s/%s/contents", + repo.RepoOwner(), repo.RepoName()) + url := RESTPrefix(repo.RepoHost()) + path + req, err := http.NewRequest("GET", url, nil) + if err != nil { + return false, err + } + + resp, err := httpClient.Do(req) + if err != nil { + return false, err + } + defer resp.Body.Close() + + if resp.StatusCode == 404 { + return false, nil + } + + if resp.StatusCode > 299 { + err = HandleHTTPError(resp) + return false, err + } + + return true, nil +} + +type releaseAsset struct { + Name string + APIURL string `json:"url"` +} + +type release struct { + Tag string `json:"tag_name"` + Assets []releaseAsset +} + +// downloadAsset downloads a single asset to the given file path. +func downloadAsset(httpClient *http.Client, asset releaseAsset, destPath string) error { + req, err := http.NewRequest("GET", asset.APIURL, nil) + if err != nil { + return err + } + + req.Header.Set("Accept", "application/octet-stream") + + resp, err := httpClient.Do(req) + if err != nil { + return err + } + defer resp.Body.Close() + + if resp.StatusCode > 299 { + return HandleHTTPError(resp) + } + + f, err := os.OpenFile(destPath, os.O_CREATE|os.O_WRONLY|os.O_TRUNC, 0755) + if err != nil { + return err + } + defer f.Close() + + _, err = io.Copy(f, resp.Body) + return err +} + +var ErrCommitNotFound = errors.New("commit not found") +var ErrReleaseNotFound = errors.New("release not found") +var ErrRepositoryNotFound = errors.New("repository not found") + +// fetchLatestRelease finds the latest published release for a repository. +func fetchLatestRelease(httpClient *http.Client, baseRepo ghrepo.Interface) (*release, error) { + path := fmt.Sprintf("repos/%s/%s/releases/latest", baseRepo.RepoOwner(), baseRepo.RepoName()) + url := RESTPrefix(baseRepo.RepoHost()) + path + req, err := http.NewRequest("GET", url, nil) + if err != nil { + return nil, err + } + + resp, err := httpClient.Do(req) + if err != nil { + return nil, err + } + defer resp.Body.Close() + + if resp.StatusCode == 404 { + return nil, ErrReleaseNotFound + } + if resp.StatusCode > 299 { + return nil, HandleHTTPError(resp) + } + + b, err := io.ReadAll(resp.Body) + if err != nil { + return nil, err + } + + var r release + err = json.Unmarshal(b, &r) + if err != nil { + return nil, err + } + + return &r, nil +} + +// fetchReleaseFromTag finds release by tag name for a repository +func fetchReleaseFromTag(httpClient *http.Client, baseRepo ghrepo.Interface, tagName string) (*release, error) { + fullRepoName := fmt.Sprintf("%s/%s", baseRepo.RepoOwner(), baseRepo.RepoName()) + path := fmt.Sprintf("repos/%s/releases/tags/%s", fullRepoName, tagName) + url := RESTPrefix(baseRepo.RepoHost()) + path + req, err := http.NewRequest("GET", url, nil) + if err != nil { + return nil, err + } + + resp, err := httpClient.Do(req) + if err != nil { + return nil, err + } + + defer resp.Body.Close() + if resp.StatusCode == 404 { + return nil, ErrReleaseNotFound + } + if resp.StatusCode > 299 { + return nil, HandleHTTPError(resp) + } + + b, err := io.ReadAll(resp.Body) + if err != nil { + return nil, err + } + + var r release + err = json.Unmarshal(b, &r) + if err != nil { + return nil, err + } + + return &r, nil +} + +func RESTPrefix(hostname string) string { + // if IsEnterprise(hostname) { + // return fmt.Sprintf("https://%s/api/v3/", hostname) + // } + if strings.EqualFold(hostname, localhost) { + return fmt.Sprintf("http://api.%s/", hostname) + } + return fmt.Sprintf("https://api.%s/", hostname) +} + +// fetchCommitSHA finds full commit SHA from a target ref in a repo +func fetchCommitSHA(httpClient *http.Client, baseRepo ghrepo.Interface, targetRef string) (string, error) { + path := fmt.Sprintf("repos/%s/%s/commits/%s", baseRepo.RepoOwner(), baseRepo.RepoName(), targetRef) + url := RESTPrefix(baseRepo.RepoHost()) + path + req, err := http.NewRequest("GET", url, nil) + if err != nil { + return "", err + } + + req.Header.Set("Accept", "application/vnd.github.v3.sha") + resp, err := httpClient.Do(req) + if err != nil { + return "", err + } + + defer resp.Body.Close() + if resp.StatusCode == 422 { + return "", ErrCommitNotFound + } + if resp.StatusCode > 299 { + return "", HandleHTTPError(resp) + } + + body, err := io.ReadAll(resp.Body) + if err != nil { + return "", err + } + + return string(body), nil +} diff --git a/pkg/cmd/extension/manager.go b/pkg/cmd/extension/manager.go new file mode 100644 index 000000000..8e56cd8d3 --- /dev/null +++ b/pkg/cmd/extension/manager.go @@ -0,0 +1,972 @@ +package extension + +import ( + "bytes" + _ "embed" + "errors" + "fmt" + "io" + "io/fs" + "net/http" + "os" + "os/exec" + "path" + "path/filepath" + "runtime" + "strings" + "sync" + + "github.com/reubenmiller/go-c8y-cli/v2/internal/ghrepo" + "github.com/reubenmiller/go-c8y-cli/v2/pkg/extensions" + "github.com/reubenmiller/go-c8y-cli/v2/pkg/findsh" + "github.com/reubenmiller/go-c8y-cli/v2/pkg/git" + + "github.com/cli/safeexec" + "github.com/reubenmiller/go-c8y-cli/v2/pkg/config" + "github.com/reubenmiller/go-c8y-cli/v2/pkg/iostreams" + "gopkg.in/yaml.v3" +) + +type Manager struct { + dataDir func() string + lookPath func(string) (string, error) + findSh func() (string, error) + newCommand func(string, ...string) *exec.Cmd + platform func() (string, string) + client *http.Client + config config.Config + io *iostreams.IOStreams + dryRunMode bool +} + +func NewManager(ios *iostreams.IOStreams, cfg *config.Config) *Manager { + return &Manager{ + dataDir: cfg.ExtensionsDataDir, + lookPath: safeexec.LookPath, + findSh: findsh.Find, + newCommand: exec.Command, + dryRunMode: cfg.DryRun(), + platform: func() (string, string) { + ext := "" + if runtime.GOOS == "windows" { + ext = ".exe" + } + return fmt.Sprintf("%s-%s", runtime.GOOS, runtime.GOARCH), ext + }, + io: ios, + } +} + +func (m *Manager) SetConfig(cfg config.Config) { + m.config = cfg +} + +func (m *Manager) SetClient(client *http.Client) { + m.client = client +} + +func (m *Manager) EnableDryRunMode() { + m.dryRunMode = true +} + +func (m *Manager) Dispatch(args []string, stdin io.Reader, stdout, stderr io.Writer) (bool, error) { + if len(args) == 0 { + return false, errors.New("too few arguments in list") + } + + var exe string + extName := args[0] + + forwardArgs := []string{} + + cArgs := strings.Join(args[1:], " ") + + exts, _ := m.list(false) + var ext Extension + found := false + for _, e := range exts { + if e.Name() == extName { + + if commands, cmdErr := e.Commands(); cmdErr == nil { + for _, c := range commands { + if strings.HasPrefix(cArgs, c.Name()) { + ext = e + + if ext.IsBinary() { + exe = filepath.Join(ext.Path()) + } else { + exe = filepath.Join(ext.Path(), commandsName) + } + + exe = filepath.Join(append([]string{exe}, strings.Split(c.Name(), " ")...)...) + + consumerArgCount := strings.Count(c.Name(), " ") + 1 + if consumerArgCount < len(args)-1 { + forwardArgs = args[consumerArgCount+1:] + } + found = true + break + } + } + } + } + + if found { + break + } + // if e.Name() == extName { + // ext = e + // if ext.IsBinary() { + // exe = filepath.Join(ext.Path()) + // } else { + // exe = filepath.Join(ext.Path(), commandsName, subCommand) + // } + // break + // } + } + if exe == "" { + return false, nil + } + + var externalCmd *exec.Cmd + + if ext.IsBinary() || runtime.GOOS != "windows" { + externalCmd = m.newCommand(exe, forwardArgs...) + } else if runtime.GOOS == "windows" { + // Dispatch all extension calls through the `sh` interpreter to support executable files with a + // shebang line on Windows. + shExe, err := m.findSh() + if err != nil { + if errors.Is(err, exec.ErrNotFound) { + return true, errors.New("the `sh.exe` interpreter is required. Please install Git for Windows and try again") + } + return true, err + } + forwardArgs = append([]string{"-c", `command "$@"`, "--", exe}, forwardArgs...) + externalCmd = m.newCommand(shExe, forwardArgs...) + } + externalCmd.Stdin = stdin + externalCmd.Stdout = stdout + externalCmd.Stderr = stderr + return true, externalCmd.Run() +} + +func (m *Manager) Execute(exe string, args []string, isBinary bool, stdin io.Reader, stdout, stderr io.Writer) (bool, error) { + forwardArgs := args[:] + + if exe == "" { + return false, nil + } + + var externalCmd *exec.Cmd + + if isBinary || runtime.GOOS != "windows" { + externalCmd = m.newCommand(exe, forwardArgs...) + } else if runtime.GOOS == "windows" { + // Dispatch all extension calls through the `sh` interpreter to support executable files with a + // shebang line on Windows. + shExe, err := m.findSh() + if err != nil { + if errors.Is(err, exec.ErrNotFound) { + return true, errors.New("the `sh.exe` interpreter is required. Please install Git for Windows and try again") + } + return true, err + } + forwardArgs = append([]string{"-c", `command "$@"`, "--", exe}, forwardArgs...) + externalCmd = m.newCommand(shExe, forwardArgs...) + } + externalCmd.Stdin = stdin + externalCmd.Stdout = stdout + externalCmd.Stderr = stderr + return true, externalCmd.Run() +} + +func (m *Manager) List() []extensions.Extension { + exts, _ := m.list(false) + r := make([]extensions.Extension, len(exts)) + for i, v := range exts { + val := v + r[i] = &val + } + return r +} + +func (m *Manager) list(includeMetadata bool) ([]Extension, error) { + dir := m.installDir() + entries, err := os.ReadDir(dir) + if err != nil { + return nil, err + } + + var results []Extension + for _, f := range entries { + if !strings.HasPrefix(f.Name(), ExtPrefix) && !strings.Contains(f.Name(), ExtPrefix) { + continue + } + var ext Extension + var err error + if f.IsDir() { + ext, err = m.parseExtensionDir(f) + if err != nil { + return nil, err + } + results = append(results, ext) + } else { + ext, err = m.parseExtensionFile(f) + if err != nil { + return nil, err + } + results = append(results, ext) + } + } + + if includeMetadata { + m.populateLatestVersions(results) + } + + return results, nil +} + +func (m *Manager) parseExtensionFile(fi fs.DirEntry) (Extension, error) { + ext := Extension{isLocal: true} + id := m.installDir() + exePath := filepath.Join(id, fi.Name()) + if !isSymlink(fi.Type()) { + // if this is a regular file, its contents is the local directory of the extension + p, err := readPathFromFile(filepath.Join(id, fi.Name())) + if err != nil { + return ext, err + } + exePath = p + } + ext.path = exePath + return ext, nil +} + +func (m *Manager) parseExtensionDir(fi fs.DirEntry) (Extension, error) { + id := m.installDir() + if _, err := os.Stat(filepath.Join(id, fi.Name(), manifestName)); err == nil { + return m.parseBinaryExtensionDir(fi) + } + + return m.parseGitExtensionDir(fi) +} + +func (m *Manager) parseBinaryExtensionDir(fi fs.DirEntry) (Extension, error) { + id := m.installDir() + exePath := filepath.Join(id, fi.Name(), fi.Name()) + ext := Extension{path: exePath, kind: BinaryKind} + manifestPath := filepath.Join(id, fi.Name(), manifestName) + manifest, err := os.ReadFile(manifestPath) + if err != nil { + return ext, fmt.Errorf("could not open %s for reading: %w", manifestPath, err) + } + var bm binManifest + err = yaml.Unmarshal(manifest, &bm) + if err != nil { + return ext, fmt.Errorf("could not parse %s: %w", manifestPath, err) + } + repo := ghrepo.NewWithHost(bm.Owner, bm.Name, bm.Host, "") + remoteURL := ghrepo.GenerateRepoURL(repo, "") + ext.url = remoteURL + ext.currentVersion = bm.Tag + ext.isPinned = bm.IsPinned + return ext, nil +} + +func (m *Manager) parseGitExtensionDir(fi fs.DirEntry) (Extension, error) { + id := m.installDir() + exePath := filepath.Join(id, fi.Name()) + remoteUrl := m.getRemoteUrl(fi.Name()) + currentVersion := m.getCurrentVersion(fi.Name()) + + var isPinned bool + pinPath := filepath.Join(id, fi.Name(), fmt.Sprintf(".pin-%s", currentVersion)) + if _, err := os.Stat(pinPath); err == nil { + isPinned = true + } + + return Extension{ + path: exePath, + url: remoteUrl, + isLocal: false, + currentVersion: currentVersion, + kind: GitKind, + isPinned: isPinned, + }, nil +} + +// getCurrentVersion determines the current version for non-local git extensions. +func (m *Manager) getCurrentVersion(extension string) string { + gitExe, err := m.lookPath("git") + if err != nil { + return "" + } + dir := m.installDir() + gitDir := "--git-dir=" + filepath.Join(dir, extension, ".git") + cmd := m.newCommand(gitExe, gitDir, "rev-parse", "HEAD") + + localSha, err := cmd.Output() + if err != nil { + return "" + } + return string(bytes.TrimSpace(localSha)) +} + +// getRemoteUrl determines the remote URL for non-local git extensions. +func (m *Manager) getRemoteUrl(extension string) string { + gitExe, err := m.lookPath("git") + if err != nil { + return "" + } + dir := m.installDir() + gitDir := "--git-dir=" + filepath.Join(dir, extension, ".git") + cmd := m.newCommand(gitExe, gitDir, "config", "remote.origin.url") + url, err := cmd.Output() + if err != nil { + return "" + } + return strings.TrimSpace(string(url)) +} + +func (m *Manager) populateLatestVersions(exts []Extension) { + size := len(exts) + type result struct { + index int + version string + } + ch := make(chan result, size) + var wg sync.WaitGroup + wg.Add(size) + for idx, ext := range exts { + go func(i int, e Extension) { + defer wg.Done() + version, _ := m.getLatestVersion(e) + ch <- result{index: i, version: version} + }(idx, ext) + } + wg.Wait() + close(ch) + for r := range ch { + ext := &exts[r.index] + ext.latestVersion = r.version + } +} + +func (m *Manager) getLatestVersion(ext Extension) (string, error) { + if ext.isLocal { + return "", ErrLocalExtensionUpgrade + } + if ext.IsBinary() { + repo, err := ghrepo.FromFullName(ext.url) + if err != nil { + return "", err + } + r, err := fetchLatestRelease(m.client, repo) + if err != nil { + return "", err + } + return r.Tag, nil + } else { + gitExe, err := m.lookPath("git") + if err != nil { + return "", err + } + extDir := ext.path + gitDir := "--git-dir=" + filepath.Join(extDir, ".git") + cmd := m.newCommand(gitExe, gitDir, "ls-remote", "origin", "HEAD") + lsRemote, err := cmd.Output() + if err != nil { + return "", err + } + remoteSha := bytes.SplitN(lsRemote, []byte("\t"), 2)[0] + return string(remoteSha), nil + } +} + +func (m *Manager) InstallLocal(dir string, name string) error { + if name == "" { + name = filepath.Base(dir) + } + targetLink := filepath.Join(m.installDir(), name) + if err := os.MkdirAll(filepath.Dir(targetLink), 0755); err != nil { + return err + } + return makeSymlink(dir, targetLink) +} + +type binManifest struct { + Owner string + Name string + Host string + Tag string + IsPinned bool + // TODO I may end up not using this; just thinking ahead to local installs + Path string +} + +// Install installs an extension from repo, and pins to commitish if provided +func (m *Manager) Install(repo ghrepo.Interface, name string, target string) error { + if strings.Contains(repo.RepoHost(), "github") { + isBin, err := isBinExtension(m.client, repo) + if err != nil { + if errors.Is(err, ErrReleaseNotFound) { + if ok, err := repoExists(m.client, repo); err != nil { + return err + } else if !ok { + return ErrRepositoryNotFound + } + } else { + return fmt.Errorf("could not check for binary extension: %w", err) + } + } + if isBin { + return m.installBin(repo, target) + } + + hb, err := hasBundle(m.client, repo) + if err != nil { + return err + } + + if !hb { + return errors.New("extension is not installable: missing executable") + } + } + + return m.installGit(repo, name, target, m.io.Out, m.io.ErrOut) +} + +func (m *Manager) installBin(repo ghrepo.Interface, target string) error { + var r *release + var err error + isPinned := target != "" + if isPinned { + r, err = fetchReleaseFromTag(m.client, repo, target) + } else { + r, err = fetchLatestRelease(m.client, repo) + } + if err != nil { + return err + } + + platform, ext := m.platform() + isMacARM := platform == "darwin-arm64" + trueARMBinary := false + + var asset *releaseAsset + for _, a := range r.Assets { + if strings.HasSuffix(a.Name, platform+ext) { + asset = &a + trueARMBinary = isMacARM + break + } + } + + // if an arm64 binary is unavailable, fall back to amd64 if it can be executed through Rosetta 2 + if asset == nil && isMacARM && hasRosetta() { + for _, a := range r.Assets { + if strings.HasSuffix(a.Name, "darwin-amd64") { + asset = &a + break + } + } + } + + if asset == nil { + return fmt.Errorf( + "%[1]s unsupported for %[2]s. Open an issue: `gh issue create -R %[3]s/%[1]s -t'Support %[2]s'`", + repo.RepoName(), platform, repo.RepoOwner()) + } + + name := repo.RepoName() + targetDir := filepath.Join(m.installDir(), name) + + // TODO clean this up if function errs? + if !m.dryRunMode { + err = os.MkdirAll(targetDir, 0755) + if err != nil { + return fmt.Errorf("failed to create installation directory: %w", err) + } + } + + binPath := filepath.Join(targetDir, name) + binPath += ext + + if !m.dryRunMode { + err = downloadAsset(m.client, *asset, binPath) + if err != nil { + return fmt.Errorf("failed to download asset %s: %w", asset.Name, err) + } + if trueARMBinary { + if err := codesignBinary(binPath); err != nil { + return fmt.Errorf("failed to codesign downloaded binary: %w", err) + } + } + } + + manifest := binManifest{ + Name: name, + Owner: repo.RepoOwner(), + Host: repo.RepoHost(), + Path: binPath, + Tag: r.Tag, + IsPinned: isPinned, + } + + bs, err := yaml.Marshal(manifest) + if err != nil { + return fmt.Errorf("failed to serialize manifest: %w", err) + } + + if !m.dryRunMode { + manifestPath := filepath.Join(targetDir, manifestName) + + f, err := os.OpenFile(manifestPath, os.O_WRONLY|os.O_CREATE|os.O_TRUNC, 0600) + if err != nil { + return fmt.Errorf("failed to open manifest for writing: %w", err) + } + defer f.Close() + + _, err = f.Write(bs) + if err != nil { + return fmt.Errorf("failed write manifest file: %w", err) + } + } + + return nil +} + +func (m *Manager) installGit(repo ghrepo.Interface, name, target string, stdout, stderr io.Writer) error { + protocol := repo.RepoHost() + cloneURL := ghrepo.FormatRemoteURL(repo, protocol) + + exe, err := m.lookPath("git") + if err != nil { + return err + } + + var commitSHA string + if target != "" { + commitSHA, err = fetchCommitSHA(m.client, repo, target) + if err != nil { + return err + } + } + + if name == "" { + name = strings.TrimSuffix(path.Base(cloneURL), ".git") + } + targetDir := filepath.Join(m.installDir(), name) + + externalCmd := m.newCommand(exe, "clone", cloneURL, targetDir) + externalCmd.Stdout = stdout + externalCmd.Stderr = stderr + if err := externalCmd.Run(); err != nil { + return err + } + if commitSHA == "" { + return nil + } + + checkoutCmd := m.newCommand(exe, "-C", targetDir, "checkout", commitSHA) + checkoutCmd.Stdout = stdout + checkoutCmd.Stderr = stderr + if err := checkoutCmd.Run(); err != nil { + return err + } + + pinPath := filepath.Join(targetDir, fmt.Sprintf(".pin-%s", commitSHA)) + f, err := os.OpenFile(pinPath, os.O_WRONLY|os.O_CREATE, 0600) + if err != nil { + return fmt.Errorf("failed to create pin file in directory: %w", err) + } + return f.Close() +} + +var ErrPinnedExtensionUpgrade = errors.New("pinned extensions can not be updated") +var ErrLocalExtensionUpgrade = errors.New("local extensions can not be updated") +var ErrUpToDate = errors.New("already up to date") +var ErrNoExtensionsInstalled = errors.New("no extensions installed") + +func (m *Manager) Upgrade(name string, force bool) error { + // Fetch metadata during list only when upgrading all extensions. + // This is a performance improvement so that we don't make a + // bunch of unnecessary network requests when trying to upgrade a single extension. + fetchMetadata := name == "" + exts, _ := m.list(fetchMetadata) + if len(exts) == 0 { + return ErrNoExtensionsInstalled + } + if name == "" { + return m.upgradeExtensions(exts, force) + } + for _, f := range exts { + if f.Name() != name { + continue + } + var err error + // For single extensions manually retrieve latest version since we forgo + // doing it during list. + f.latestVersion, err = m.getLatestVersion(f) + if err != nil { + return err + } + return m.upgradeExtension(f, force) + } + return fmt.Errorf("no extension matched %q", name) +} + +func (m *Manager) upgradeExtensions(exts []Extension, force bool) error { + var failed bool + for _, f := range exts { + fmt.Fprintf(m.io.Out, "[%s]: ", f.Name()) + err := m.upgradeExtension(f, force) + if err != nil { + if !errors.Is(err, ErrLocalExtensionUpgrade) && + !errors.Is(err, ErrUpToDate) && + !errors.Is(err, ErrPinnedExtensionUpgrade) { + failed = true + } + fmt.Fprintf(m.io.Out, "%s\n", err) + continue + } + currentVersion := displayExtensionVersion(&f, f.currentVersion) + latestVersion := displayExtensionVersion(&f, f.latestVersion) + if m.dryRunMode { + fmt.Fprintf(m.io.Out, "would have upgraded from %s to %s\n", currentVersion, latestVersion) + } else { + fmt.Fprintf(m.io.Out, "upgraded from %s to %s\n", currentVersion, latestVersion) + } + } + if failed { + return errors.New("some extensions failed to upgrade") + } + return nil +} + +func (m *Manager) upgradeExtension(ext Extension, force bool) error { + if ext.isLocal { + return ErrLocalExtensionUpgrade + } + if ext.IsPinned() { + return ErrPinnedExtensionUpgrade + } + if !ext.UpdateAvailable() { + return ErrUpToDate + } + var err error + if ext.IsBinary() { + err = m.upgradeBinExtension(ext) + } else { + // Check if git extension has changed to a binary extension + var isBin bool + repo, repoErr := repoFromPath(ext.Path()) + if repoErr == nil { + isBin, _ = isBinExtension(m.client, repo) + } + if isBin { + if err := m.Remove(ext.Name()); err != nil { + return fmt.Errorf("failed to migrate to new precompiled extension format: %w", err) + } + return m.installBin(repo, "") + } + err = m.upgradeGitExtension(ext, force) + } + return err +} + +func (m *Manager) upgradeGitExtension(ext Extension, force bool) error { + exe, err := m.lookPath("git") + if err != nil { + return err + } + dir := ext.path + if m.dryRunMode { + return nil + } + if force { + if err := m.newCommand(exe, "-C", dir, "fetch", "origin", "HEAD").Run(); err != nil { + return err + } + return m.newCommand(exe, "-C", dir, "reset", "--hard", "origin/HEAD").Run() + } + return m.newCommand(exe, "-C", dir, "pull", "--ff-only").Run() +} + +func (m *Manager) upgradeBinExtension(ext Extension) error { + repo, err := ghrepo.FromFullName(ext.url) + if err != nil { + return fmt.Errorf("failed to parse URL %s: %w", ext.url, err) + } + return m.installBin(repo, "") +} + +func (m *Manager) Remove(name string) error { + if strings.TrimSpace(name) == "" { + return fmt.Errorf("extension name is empty") + } + targetDirs := []string{ + filepath.Join(m.installDir(), ExtPrefix+name), + filepath.Join(m.installDir(), name), + } + + var targetDir string + var found = false + + for _, targetDir = range targetDirs { + if _, err := os.Lstat(targetDir); os.IsNotExist(err) { + continue + } + found = true + break + } + + if !found { + return fmt.Errorf("no extension found: %q", name) + } + + if m.dryRunMode { + return nil + } + return os.RemoveAll(targetDir) +} + +func (m *Manager) installDir() string { + return m.dataDir() +} + +//go:embed ext_tmpls/script.sh +var scriptTmpl string + +//go:embed ext_tmpls/script-inventory-example.sh +var scriptInventoryTmpl string + +//go:embed ext_tmpls/README.md +var readmeTmpl string + +//go:embed ext_tmpls/extension.yaml +var exampleExtensionManifest []byte + +//go:embed ext_tmpls/customCommand.jsonnet +var exampleJsonnet []byte + +//go:embed ext_tmpls/exampleDevice.json +var exampleView []byte + +//go:embed ext_tmpls/apiCommandTemplate.yaml +var commandGroupTmpl string + +func (m *Manager) Create(name string, tmplType extensions.ExtTemplateType) error { + exe, err := m.lookPath("git") + if err != nil { + return err + } + + cmdName := strings.TrimPrefix(name, ExtPrefix) + + if err := m.newCommand(exe, "init", "--quiet", name).Run(); err != nil { + return err + } + + if err := writeFile(filepath.Join(name, "extension.yaml"), exampleExtensionManifest, 0755); err != nil { + return err + } + + readme := fmt.Sprintf(readmeTmpl, name) + if err := writeFile(filepath.Join(name, "README.md"), []byte(readme), 0644); err != nil { + return err + } + + commandsDir := filepath.Join(name, commandsName) + if err := os.MkdirAll(commandsDir, 0755); err != nil { + return err + } + subCommandsDir := filepath.Join(name, commandsName, "services") + if err := os.MkdirAll(subCommandsDir, 0755); err != nil { + return err + } + + apiDir := filepath.Join(name, apiName) + if err := os.MkdirAll(apiDir, 0755); err != nil { + return err + } + if err := writeFile(filepath.Join(apiDir, "devices.yaml"), []byte(commandGroupTmpl), 0644); err != nil { + return err + } + + templatesDir := filepath.Join(name, templateName) + if err := os.MkdirAll(templatesDir, 0755); err != nil { + return err + } + if err := writeFile(filepath.Join(templatesDir, "customCommand.jsonnet"), exampleJsonnet, 0644); err != nil { + return err + } + + viewsDir := filepath.Join(name, viewsName) + if err := os.MkdirAll(viewsDir, 0755); err != nil { + return err + } + if err := writeFile(filepath.Join(viewsDir, "exampleDevice.json"), exampleView, 0644); err != nil { + return err + } + + if tmplType == extensions.GoBinTemplateType { + return nil + } else if tmplType == extensions.OtherBinTemplateType { + return nil + } + + script := fmt.Sprintf(scriptTmpl, cmdName, "services list") + if err := writeFile(filepath.Join(subCommandsDir, "list"), []byte(script), 0755); err != nil { + return err + } + m.newCommand(exe, "-C", name, "add", filepath.Join(commandsName, "services", "list"), "--chmod=+x").Run() + + // stage remaining files + return m.newCommand(exe, "-C", name, "add", "**").Run() +} + +func isSymlink(m os.FileMode) bool { + return m&os.ModeSymlink != 0 +} + +func writeFile(p string, contents []byte, mode os.FileMode) error { + if dir := filepath.Dir(p); dir != "." { + if err := os.MkdirAll(dir, 0755); err != nil { + return err + } + } + return os.WriteFile(p, contents, mode) +} + +// reads the product of makeSymlink on Windows +func readPathFromFile(path string) (string, error) { + f, err := os.Open(path) + if err != nil { + return "", err + } + defer f.Close() + b := make([]byte, 1024) + n, err := f.Read(b) + return strings.TrimSpace(string(b[:n])), err +} + +func isBinExtension(client *http.Client, repo ghrepo.Interface) (isBin bool, err error) { + var r *release + r, err = fetchLatestRelease(client, repo) + if err != nil { + return + } + + for _, a := range r.Assets { + dists := possibleDists() + for _, d := range dists { + suffix := d + if strings.HasPrefix(d, "windows") { + suffix += ".exe" + } + if strings.HasSuffix(a.Name, suffix) { + isBin = true + break + } + } + } + + return +} + +func repoFromPath(path string) (ghrepo.Interface, error) { + remotes, err := git.RemotesForPath(path) + if err != nil { + return nil, err + } + + if len(remotes) == 0 { + return nil, fmt.Errorf("no remotes configured for %s", path) + } + + var remote *git.Remote + + for _, r := range remotes { + if r.Name == "origin" { + remote = r + break + } + } + + if remote == nil { + remote = remotes[0] + } + + return ghrepo.FromURL(remote.FetchURL) +} + +func possibleDists() []string { + return []string{ + "aix-ppc64", + "android-386", + "android-amd64", + "android-arm", + "android-arm64", + "darwin-amd64", + "darwin-arm64", + "dragonfly-amd64", + "freebsd-386", + "freebsd-amd64", + "freebsd-arm", + "freebsd-arm64", + "illumos-amd64", + "ios-amd64", + "ios-arm64", + "js-wasm", + "linux-386", + "linux-amd64", + "linux-arm", + "linux-arm64", + "linux-mips", + "linux-mips64", + "linux-mips64le", + "linux-mipsle", + "linux-ppc64", + "linux-ppc64le", + "linux-riscv64", + "linux-s390x", + "netbsd-386", + "netbsd-amd64", + "netbsd-arm", + "netbsd-arm64", + "openbsd-386", + "openbsd-amd64", + "openbsd-arm", + "openbsd-arm64", + "openbsd-mips64", + "plan9-386", + "plan9-amd64", + "plan9-arm", + "solaris-amd64", + "windows-386", + "windows-amd64", + "windows-arm", + "windows-arm64", + } +} + +func hasRosetta() bool { + _, err := os.Stat("/Library/Apple/usr/libexec/oah/libRosettaRuntime") + return err == nil +} + +func codesignBinary(binPath string) error { + codesignExe, err := safeexec.LookPath("codesign") + if err != nil { + return err + } + cmd := exec.Command(codesignExe, "--sign", "-", "--force", "--preserve-metadata=entitlements,requirements,flags,runtime", binPath) + return cmd.Run() +} + +type AliasCollection struct { + Name string + Aliases []extensions.Alias +} diff --git a/pkg/cmd/extension/symlink_other.go b/pkg/cmd/extension/symlink_other.go new file mode 100644 index 000000000..59c1989d9 --- /dev/null +++ b/pkg/cmd/extension/symlink_other.go @@ -0,0 +1,10 @@ +//go:build !windows +// +build !windows + +package extension + +import "os" + +func makeSymlink(oldname, newname string) error { + return os.Symlink(oldname, newname) +} diff --git a/pkg/cmd/extension/symlink_windows.go b/pkg/cmd/extension/symlink_windows.go new file mode 100644 index 000000000..5f29e3fb1 --- /dev/null +++ b/pkg/cmd/extension/symlink_windows.go @@ -0,0 +1,15 @@ +package extension + +import "os" + +func makeSymlink(oldname, newname string) error { + // Create a regular file that contains the location of the directory where to find this extension. We + // avoid relying on symlinks because creating them on Windows requires administrator privileges. + f, err := os.OpenFile(newname, os.O_WRONLY|os.O_CREATE, 0644) + if err != nil { + return err + } + defer f.Close() + _, err = f.WriteString(oldname) + return err +} diff --git a/pkg/cmd/factory/factory.go b/pkg/cmd/factory/factory.go index adbfa895f..3263258fd 100644 --- a/pkg/cmd/factory/factory.go +++ b/pkg/cmd/factory/factory.go @@ -1,13 +1,18 @@ package factory import ( + "crypto/tls" + "net/http" "os" + "time" "github.com/reubenmiller/go-c8y-cli/v2/pkg/activitylogger" + "github.com/reubenmiller/go-c8y-cli/v2/pkg/cmd/extension" "github.com/reubenmiller/go-c8y-cli/v2/pkg/cmdutil" "github.com/reubenmiller/go-c8y-cli/v2/pkg/config" "github.com/reubenmiller/go-c8y-cli/v2/pkg/console" "github.com/reubenmiller/go-c8y-cli/v2/pkg/dataview" + "github.com/reubenmiller/go-c8y-cli/v2/pkg/extensions" "github.com/reubenmiller/go-c8y-cli/v2/pkg/iostreams" "github.com/reubenmiller/go-c8y-cli/v2/pkg/logger" "github.com/reubenmiller/go-c8y/pkg/c8y" @@ -34,6 +39,10 @@ func New(appVersion string, buildBranch string, configFunc func() (*config.Confi BuildBranch: buildBranch, } f.Browser = browser(f) + f.ExtensionManager = func() extensions.ExtensionManager { + // cfg, err := f.Config() + return extensionManager(f) + } return f } @@ -56,3 +65,34 @@ func browserLauncher(f *cmdutil.Factory) string { return os.Getenv("BROWSER") } + +func extensionManager(f *cmdutil.Factory) *extension.Manager { + cfg, err := f.Config() + if err != nil { + return extension.NewManager(f.IOStreams, cfg) + } + em := extension.NewManager(f.IOStreams, cfg) + + defaultTransport := http.DefaultTransport.(*http.Transport) + tr := &http.Transport{ + Proxy: defaultTransport.Proxy, + DialContext: defaultTransport.DialContext, + MaxIdleConns: defaultTransport.MaxIdleConns, + IdleConnTimeout: defaultTransport.IdleConnTimeout, + ExpectContinueTimeout: defaultTransport.ExpectContinueTimeout, + TLSHandshakeTimeout: defaultTransport.TLSHandshakeTimeout, + TLSClientConfig: &tls.Config{ + InsecureSkipVerify: false, + }, + } + + httpClient := &http.Client{ + Transport: tr, + Timeout: time.Second * 30, + } + + em.SetClient(httpClient) + // em.SetClient(api.NewCachedHTTPClient(client, time.Second*30)) + + return em +} diff --git a/pkg/cmd/firmware/create/create.auto.go b/pkg/cmd/firmware/create/create.auto.go index 680c23bcd..737622879 100644 --- a/pkg/cmd/firmware/create/create.auto.go +++ b/pkg/cmd/firmware/create/create.auto.go @@ -145,7 +145,7 @@ func (n *CreateCmd) RunE(cmd *cobra.Command, args []string) error { flags.WithStringValue("deviceType", "c8y_Filter.type"), flags.WithDefaultTemplateString(` {type: 'c8y_Firmware', c8y_Global:{}}`), - cmdutil.WithTemplateValue(cfg), + cmdutil.WithTemplateValue(n.factory), flags.WithTemplateVariablesValue(), flags.WithRequiredProperties("type", "name"), ) diff --git a/pkg/cmd/firmware/delete/delete.auto.go b/pkg/cmd/firmware/delete/delete.auto.go index b47a2fd77..2abe5cbd7 100644 --- a/pkg/cmd/firmware/delete/delete.auto.go +++ b/pkg/cmd/firmware/delete/delete.auto.go @@ -150,7 +150,7 @@ func (n *DeleteCmd) RunE(cmd *cobra.Command, args []string) error { cmd, path, inputIterators, - c8yfetcher.WithFirmwareByNameFirstMatch(client, args, "id", "id"), + c8yfetcher.WithFirmwareByNameFirstMatch(n.factory, args, "id", "id"), ) if err != nil { return err diff --git a/pkg/cmd/firmware/get/get.auto.go b/pkg/cmd/firmware/get/get.auto.go index 83cea8308..18fa3e5d1 100644 --- a/pkg/cmd/firmware/get/get.auto.go +++ b/pkg/cmd/firmware/get/get.auto.go @@ -162,7 +162,7 @@ func (n *GetCmd) RunE(cmd *cobra.Command, args []string) error { cmd, path, inputIterators, - c8yfetcher.WithFirmwareByNameFirstMatch(client, args, "id", "id"), + c8yfetcher.WithFirmwareByNameFirstMatch(n.factory, args, "id", "id"), ) if err != nil { return err diff --git a/pkg/cmd/firmware/patches/create/create.manual.go b/pkg/cmd/firmware/patches/create/create.manual.go index b7c360461..74d7cb6d8 100644 --- a/pkg/cmd/firmware/patches/create/create.manual.go +++ b/pkg/cmd/firmware/patches/create/create.manual.go @@ -151,7 +151,7 @@ func (n *CreateCmd) RunE(cmd *cobra.Command, args []string) error { flags.WithStringValue("dependencyVersion", "c8y_Patch.dependency"), flags.WithDefaultTemplateString(` {type: 'c8y_FirmwareBinary', c8y_Global:{}}`), - cmdutil.WithTemplateValue(cfg), + cmdutil.WithTemplateValue(n.factory), flags.WithTemplateVariablesValue(), flags.WithRequiredProperties("type", "c8y_Firmware.version", "c8y_Patch.dependency"), ) @@ -165,7 +165,7 @@ func (n *CreateCmd) RunE(cmd *cobra.Command, args []string) error { cmd, path, inputIterators, - c8yfetcher.WithFirmwareByNameFirstMatch(client, args, "firmware", "firmware"), + c8yfetcher.WithFirmwareByNameFirstMatch(n.factory, args, "firmware", "firmware"), ) if err != nil { return err diff --git a/pkg/cmd/firmware/patches/delete/delete.auto.go b/pkg/cmd/firmware/patches/delete/delete.auto.go index 28e3da45d..34a268f9d 100644 --- a/pkg/cmd/firmware/patches/delete/delete.auto.go +++ b/pkg/cmd/firmware/patches/delete/delete.auto.go @@ -152,8 +152,8 @@ func (n *DeleteCmd) RunE(cmd *cobra.Command, args []string) error { cmd, path, inputIterators, - c8yfetcher.WithFirmwarePatchByNameFirstMatch(client, args, "id", "id"), - c8yfetcher.WithFirmwareByNameFirstMatch(client, args, "firmware", "firmware"), + c8yfetcher.WithFirmwarePatchByNameFirstMatch(n.factory, "firmware", args, "id", "id"), + c8yfetcher.WithFirmwareByNameFirstMatch(n.factory, args, "firmware", "firmware"), ) if err != nil { return err diff --git a/pkg/cmd/firmware/patches/get/get.auto.go b/pkg/cmd/firmware/patches/get/get.auto.go index e8ce0410a..8e199dde3 100644 --- a/pkg/cmd/firmware/patches/get/get.auto.go +++ b/pkg/cmd/firmware/patches/get/get.auto.go @@ -161,8 +161,8 @@ func (n *GetCmd) RunE(cmd *cobra.Command, args []string) error { cmd, path, inputIterators, - c8yfetcher.WithFirmwarePatchByNameFirstMatch(client, args, "id", "id"), - c8yfetcher.WithFirmwareByNameFirstMatch(client, args, "firmware", "firmware"), + c8yfetcher.WithFirmwarePatchByNameFirstMatch(n.factory, "firmware", args, "id", "id"), + c8yfetcher.WithFirmwareByNameFirstMatch(n.factory, args, "firmware", "firmware"), ) if err != nil { return err diff --git a/pkg/cmd/firmware/patches/list/list.auto.go b/pkg/cmd/firmware/patches/list/list.auto.go index 18a5e0eb8..b757d39cb 100644 --- a/pkg/cmd/firmware/patches/list/list.auto.go +++ b/pkg/cmd/firmware/patches/list/list.auto.go @@ -118,7 +118,7 @@ func (n *ListCmd) RunE(cmd *cobra.Command, args []string) error { flags.WithCumulocityQuery( []flags.GetOption{ flags.WithStringValue("query", "query", "%s"), - c8yfetcher.WithFirmwareByNameFirstMatch(client, args, "firmware", "firmware", "bygroupid(%s)"), + c8yfetcher.WithFirmwareByNameFirstMatch(n.factory, args, "firmware", "firmware", "bygroupid(%s)"), flags.WithStaticStringValue("ignorePatches", "has(c8y_Patch)"), flags.WithStringValue("dependency", "dependency", "(c8y_Patch.dependency eq '%s')"), flags.WithStringValue("version", "version", "(c8y_Firmware.version eq '%s')"), diff --git a/pkg/cmd/firmware/update/update.auto.go b/pkg/cmd/firmware/update/update.auto.go index 232a31b1b..75fffa004 100644 --- a/pkg/cmd/firmware/update/update.auto.go +++ b/pkg/cmd/firmware/update/update.auto.go @@ -145,7 +145,7 @@ func (n *UpdateCmd) RunE(cmd *cobra.Command, args []string) error { flags.WithStringValue("newName", "name"), flags.WithStringValue("description", "description"), flags.WithStringValue("deviceType", "c8y_Filter.type"), - cmdutil.WithTemplateValue(cfg), + cmdutil.WithTemplateValue(n.factory), flags.WithTemplateVariablesValue(), ) if err != nil { @@ -158,7 +158,7 @@ func (n *UpdateCmd) RunE(cmd *cobra.Command, args []string) error { cmd, path, inputIterators, - c8yfetcher.WithFirmwareByNameFirstMatch(client, args, "id", "id"), + c8yfetcher.WithFirmwareByNameFirstMatch(n.factory, args, "id", "id"), ) if err != nil { return err diff --git a/pkg/cmd/firmware/versions/create/create.manual.go b/pkg/cmd/firmware/versions/create/create.manual.go index 81bff7e14..43c28d5c3 100644 --- a/pkg/cmd/firmware/versions/create/create.manual.go +++ b/pkg/cmd/firmware/versions/create/create.manual.go @@ -144,7 +144,7 @@ func (n *CreateCmd) RunE(cmd *cobra.Command, args []string) error { flags.WithStringValue("url", "c8y_Firmware.url"), flags.WithDefaultTemplateString(` {type: 'c8y_FirmwareBinary', c8y_Global:{}}`), - cmdutil.WithTemplateValue(cfg), + cmdutil.WithTemplateValue(n.factory), flags.WithTemplateVariablesValue(), flags.WithRequiredProperties("type", "c8y_Firmware.version"), ) @@ -158,7 +158,7 @@ func (n *CreateCmd) RunE(cmd *cobra.Command, args []string) error { cmd, path, inputIterators, - c8yfetcher.WithFirmwareByNameFirstMatch(client, args, "firmware", "firmware"), + c8yfetcher.WithFirmwareByNameFirstMatch(n.factory, args, "firmware", "firmware"), ) if err != nil { return err diff --git a/pkg/cmd/firmware/versions/delete/delete.auto.go b/pkg/cmd/firmware/versions/delete/delete.auto.go index 5d5af4704..d693bc25f 100644 --- a/pkg/cmd/firmware/versions/delete/delete.auto.go +++ b/pkg/cmd/firmware/versions/delete/delete.auto.go @@ -152,8 +152,8 @@ func (n *DeleteCmd) RunE(cmd *cobra.Command, args []string) error { cmd, path, inputIterators, - c8yfetcher.WithFirmwareVersionByNameFirstMatch(client, args, "id", "id"), - c8yfetcher.WithFirmwareByNameFirstMatch(client, args, "firmware", "firmware"), + c8yfetcher.WithFirmwareVersionByNameFirstMatch(n.factory, "firmware", args, "id", "id"), + c8yfetcher.WithFirmwareByNameFirstMatch(n.factory, args, "firmware", "firmware"), ) if err != nil { return err diff --git a/pkg/cmd/firmware/versions/get/get.auto.go b/pkg/cmd/firmware/versions/get/get.auto.go index d062b3ffc..ad035f3ff 100644 --- a/pkg/cmd/firmware/versions/get/get.auto.go +++ b/pkg/cmd/firmware/versions/get/get.auto.go @@ -164,8 +164,8 @@ func (n *GetCmd) RunE(cmd *cobra.Command, args []string) error { cmd, path, inputIterators, - c8yfetcher.WithFirmwareVersionByNameFirstMatch(client, args, "id", "id"), - c8yfetcher.WithFirmwareByNameFirstMatch(client, args, "firmware", "firmware"), + c8yfetcher.WithFirmwareVersionByNameFirstMatch(n.factory, "firmware", args, "id", "id"), + c8yfetcher.WithFirmwareByNameFirstMatch(n.factory, args, "firmware", "firmware"), ) if err != nil { return err diff --git a/pkg/cmd/firmware/versions/install/install.auto.go b/pkg/cmd/firmware/versions/install/install.auto.go index 3b50a2351..16d1e5a57 100644 --- a/pkg/cmd/firmware/versions/install/install.auto.go +++ b/pkg/cmd/firmware/versions/install/install.auto.go @@ -149,11 +149,11 @@ func (n *InstallCmd) RunE(cmd *cobra.Command, args []string) error { body, inputIterators, flags.WithDataFlagValue(), - c8yfetcher.WithDeviceByNameFirstMatch(client, args, "device", "deviceId"), + c8yfetcher.WithDeviceByNameFirstMatch(n.factory, args, "device", "deviceId"), flags.WithStringValue("firmware", "c8y_Firmware.name"), flags.WithStringValue("version", "c8y_Firmware.version"), flags.WithStringValue("url", "c8y_Firmware.url"), - c8yfetcher.WithFirmwareVersionData(client, "firmware", "version", "url", args, "", "c8y_Firmware"), + c8yfetcher.WithFirmwareVersionData(n.factory, "firmware", "version", "url", args, "", "c8y_Firmware"), flags.WithStringValue("description", "description"), flags.WithDefaultTemplateString(` { @@ -163,7 +163,7 @@ func (n *InstallCmd) RunE(cmd *cobra.Command, args []string) error { + (if self._version != "" then " (%s)" % self._version else "") } `), - cmdutil.WithTemplateValue(cfg), + cmdutil.WithTemplateValue(n.factory), flags.WithTemplateVariablesValue(), flags.WithRequiredProperties("deviceId", "c8y_Firmware.name", "c8y_Firmware.version"), ) diff --git a/pkg/cmd/firmware/versions/list/list.auto.go b/pkg/cmd/firmware/versions/list/list.auto.go index 8e26d2b59..83da3963f 100644 --- a/pkg/cmd/firmware/versions/list/list.auto.go +++ b/pkg/cmd/firmware/versions/list/list.auto.go @@ -120,7 +120,7 @@ func (n *ListCmd) RunE(cmd *cobra.Command, args []string) error { flags.WithCumulocityQuery( []flags.GetOption{ flags.WithStringValue("query", "query", "%s"), - c8yfetcher.WithFirmwareByNameFirstMatch(client, args, "firmware", "firmware", "bygroupid(%s)"), + c8yfetcher.WithFirmwareByNameFirstMatch(n.factory, args, "firmware", "firmware", "bygroupid(%s)"), flags.WithStaticStringValue("ignorePatches", "not(has(c8y_Patch))"), flags.WithStringValue("version", "version", "(c8y_Firmware.version eq '%s')"), flags.WithStringValue("url", "url", "(c8y_Firmware.url eq '%s')"), diff --git a/pkg/cmd/identity/create/create.auto.go b/pkg/cmd/identity/create/create.auto.go index b94e3a0c4..5a19182e2 100644 --- a/pkg/cmd/identity/create/create.auto.go +++ b/pkg/cmd/identity/create/create.auto.go @@ -147,7 +147,7 @@ func (n *CreateCmd) RunE(cmd *cobra.Command, args []string) error { flags.WithStringValue("name", "externalId"), flags.WithDefaultTemplateString(` {type: 'c8y_Serial'}`), - cmdutil.WithTemplateValue(cfg), + cmdutil.WithTemplateValue(n.factory), flags.WithTemplateVariablesValue(), flags.WithRequiredProperties("type", "externalId"), ) @@ -161,7 +161,7 @@ func (n *CreateCmd) RunE(cmd *cobra.Command, args []string) error { cmd, path, inputIterators, - c8yfetcher.WithDeviceByNameFirstMatch(client, args, "device", "device"), + c8yfetcher.WithDeviceByNameFirstMatch(n.factory, args, "device", "device"), ) if err != nil { return err diff --git a/pkg/cmd/identity/list/list.auto.go b/pkg/cmd/identity/list/list.auto.go index 2fa2c6355..9b311f3de 100644 --- a/pkg/cmd/identity/list/list.auto.go +++ b/pkg/cmd/identity/list/list.auto.go @@ -155,7 +155,7 @@ func (n *ListCmd) RunE(cmd *cobra.Command, args []string) error { cmd, path, inputIterators, - c8yfetcher.WithDeviceByNameFirstMatch(client, args, "device", "device"), + c8yfetcher.WithDeviceByNameFirstMatch(n.factory, args, "device", "device"), ) if err != nil { return err diff --git a/pkg/cmd/inventory/additions/assign/assign.auto.go b/pkg/cmd/inventory/additions/assign/assign.auto.go index 0b1c87d88..8882d253e 100644 --- a/pkg/cmd/inventory/additions/assign/assign.auto.go +++ b/pkg/cmd/inventory/additions/assign/assign.auto.go @@ -140,7 +140,7 @@ func (n *AssignCmd) RunE(cmd *cobra.Command, args []string) error { flags.WithOverrideValue("child", "managedObject.id"), flags.WithDataFlagValue(), flags.WithStringValue("child", "managedObject.id"), - cmdutil.WithTemplateValue(cfg), + cmdutil.WithTemplateValue(n.factory), flags.WithTemplateVariablesValue(), ) if err != nil { diff --git a/pkg/cmd/inventory/additions/create/create.auto.go b/pkg/cmd/inventory/additions/create/create.auto.go index c77027222..c80be3abc 100644 --- a/pkg/cmd/inventory/additions/create/create.auto.go +++ b/pkg/cmd/inventory/additions/create/create.auto.go @@ -138,7 +138,7 @@ func (n *CreateCmd) RunE(cmd *cobra.Command, args []string) error { inputIterators, flags.WithDataFlagValue(), flags.WithOptionalFragment("global", "c8y_Global", ""), - cmdutil.WithTemplateValue(cfg), + cmdutil.WithTemplateValue(n.factory), flags.WithTemplateVariablesValue(), ) if err != nil { diff --git a/pkg/cmd/inventory/assert/factory/factory.manual.go b/pkg/cmd/inventory/assert/factory/factory.manual.go index 58dc31ae1..029cd160c 100644 --- a/pkg/cmd/inventory/assert/factory/factory.manual.go +++ b/pkg/cmd/inventory/assert/factory/factory.manual.go @@ -212,7 +212,7 @@ func NewAssertDeviceCmdFactory(cmd *cobra.Command, f *cmdutil.Factory, h StateCh cmd, path, inputIterators, - c8yfetcher.WithDeviceByNameFirstMatch(client, args, "device", "device"), + c8yfetcher.WithDeviceByNameFirstMatch(f, args, "device", "device"), ) if err != nil { return err diff --git a/pkg/cmd/inventory/assets/assign/assign.auto.go b/pkg/cmd/inventory/assets/assign/assign.auto.go index e79bad5ff..76eb8f696 100644 --- a/pkg/cmd/inventory/assets/assign/assign.auto.go +++ b/pkg/cmd/inventory/assets/assign/assign.auto.go @@ -143,9 +143,9 @@ func (n *AssignCmd) RunE(cmd *cobra.Command, args []string) error { body, inputIterators, flags.WithDataFlagValue(), - c8yfetcher.WithDeviceByNameFirstMatch(client, args, "childDevice", "managedObject.id"), - c8yfetcher.WithDeviceGroupByNameFirstMatch(client, args, "childGroup", "managedObject.id"), - cmdutil.WithTemplateValue(cfg), + c8yfetcher.WithDeviceByNameFirstMatch(n.factory, args, "childDevice", "managedObject.id"), + c8yfetcher.WithDeviceGroupByNameFirstMatch(n.factory, args, "childGroup", "managedObject.id"), + cmdutil.WithTemplateValue(n.factory), flags.WithTemplateVariablesValue(), flags.WithRequiredProperties("managedObject"), ) diff --git a/pkg/cmd/inventory/children/assign/assign.auto.go b/pkg/cmd/inventory/children/assign/assign.auto.go index 6dd2565a3..14f517247 100644 --- a/pkg/cmd/inventory/children/assign/assign.auto.go +++ b/pkg/cmd/inventory/children/assign/assign.auto.go @@ -143,7 +143,7 @@ func (n *AssignCmd) RunE(cmd *cobra.Command, args []string) error { flags.WithOverrideValue("child", "managedObject.id"), flags.WithDataFlagValue(), flags.WithStringValue("child", "managedObject.id"), - cmdutil.WithTemplateValue(cfg), + cmdutil.WithTemplateValue(n.factory), flags.WithTemplateVariablesValue(), ) if err != nil { diff --git a/pkg/cmd/inventory/children/create/create.auto.go b/pkg/cmd/inventory/children/create/create.auto.go index 0dc44d323..d44b419c3 100644 --- a/pkg/cmd/inventory/children/create/create.auto.go +++ b/pkg/cmd/inventory/children/create/create.auto.go @@ -141,7 +141,7 @@ func (n *CreateCmd) RunE(cmd *cobra.Command, args []string) error { inputIterators, flags.WithDataFlagValue(), flags.WithOptionalFragment("global", "c8y_Global", ""), - cmdutil.WithTemplateValue(cfg), + cmdutil.WithTemplateValue(n.factory), flags.WithTemplateVariablesValue(), ) if err != nil { diff --git a/pkg/cmd/inventory/count/count.auto.go b/pkg/cmd/inventory/count/count.auto.go index 43eb3a510..ab900833b 100644 --- a/pkg/cmd/inventory/count/count.auto.go +++ b/pkg/cmd/inventory/count/count.auto.go @@ -120,7 +120,7 @@ func (n *CountCmd) RunE(cmd *cobra.Command, args []string) error { flags.WithStringValue("text", "text"), flags.WithStringValue("childAdditionId", "childAdditionId"), flags.WithStringValue("childAssetId", "childAssetId"), - c8yfetcher.WithDeviceByNameFirstMatch(client, args, "childDeviceId", "childDeviceId"), + c8yfetcher.WithDeviceByNameFirstMatch(n.factory, args, "childDeviceId", "childDeviceId"), ) if err != nil { return cmderrors.NewUserError(err) diff --git a/pkg/cmd/inventory/create/create.auto.go b/pkg/cmd/inventory/create/create.auto.go index ae5f42029..233afad35 100644 --- a/pkg/cmd/inventory/create/create.auto.go +++ b/pkg/cmd/inventory/create/create.auto.go @@ -138,7 +138,7 @@ func (n *CreateCmd) RunE(cmd *cobra.Command, args []string) error { flags.WithDataFlagValue(), flags.WithStringValue("name", "name"), flags.WithStringValue("type", "type"), - cmdutil.WithTemplateValue(cfg), + cmdutil.WithTemplateValue(n.factory), flags.WithTemplateVariablesValue(), ) if err != nil { diff --git a/pkg/cmd/inventory/find/find.auto.go b/pkg/cmd/inventory/find/find.auto.go index a9e27e024..98ad9657b 100644 --- a/pkg/cmd/inventory/find/find.auto.go +++ b/pkg/cmd/inventory/find/find.auto.go @@ -147,7 +147,7 @@ func (n *FindCmd) RunE(cmd *cobra.Command, args []string) error { flags.WithEncodedRelativeTimestamp("lastMessageDateFrom", "lastMessageDateFrom", "(c8y_Availability.lastMessage ge '%s')"), flags.WithEncodedRelativeTimestamp("creationTimeDateTo", "creationTimeDateTo", "(creationTime.date le '%s')"), flags.WithEncodedRelativeTimestamp("creationTimeDateFrom", "creationTimeDateFrom", "(creationTime.date ge '%s')"), - c8yfetcher.WithDeviceGroupByNameFirstMatch(client, args, "group", "group", "bygroupid(%s)"), + c8yfetcher.WithDeviceGroupByNameFirstMatch(n.factory, args, "group", "group", "bygroupid(%s)"), flags.WithDefaultBoolValue("onlyDevices", "onlyDevices", "has(c8y_IsDevice)"), }, "query", diff --git a/pkg/cmd/inventory/list/list.auto.go b/pkg/cmd/inventory/list/list.auto.go index 298dbfb19..e34cf09d7 100644 --- a/pkg/cmd/inventory/list/list.auto.go +++ b/pkg/cmd/inventory/list/list.auto.go @@ -126,7 +126,7 @@ func (n *ListCmd) RunE(cmd *cobra.Command, args []string) error { flags.WithBoolValue("onlyRoots", "onlyRoots", ""), flags.WithStringValue("childAdditionId", "childAdditionId"), flags.WithStringValue("childAssetId", "childAssetId"), - c8yfetcher.WithDeviceByNameFirstMatch(client, args, "childDeviceId", "childDeviceId"), + c8yfetcher.WithDeviceByNameFirstMatch(n.factory, args, "childDeviceId", "childDeviceId"), flags.WithBoolValue("skipChildrenNames", "skipChildrenNames", ""), flags.WithBoolValue("withParents", "withParents", ""), flags.WithBoolValue("withChildren", "withChildren", ""), diff --git a/pkg/cmd/inventory/subscribe/subscribe.manual.go b/pkg/cmd/inventory/subscribe/subscribe.manual.go index 57f41e0ca..81d7cac60 100644 --- a/pkg/cmd/inventory/subscribe/subscribe.manual.go +++ b/pkg/cmd/inventory/subscribe/subscribe.manual.go @@ -93,7 +93,7 @@ func (n *CmdSubscribe) RunE(cmd *cobra.Command, args []string) error { path, inputIterators, flags.WithStringDefaultValue("*", "device", "device"), - c8yfetcher.WithDeviceByNameFirstMatch(client, args, "device", "device"), + c8yfetcher.WithDeviceByNameFirstMatch(n.factory, args, "device", "device"), ) if err != nil { return err diff --git a/pkg/cmd/inventory/update/update.auto.go b/pkg/cmd/inventory/update/update.auto.go index 2cfe60970..5d9cd6eb5 100644 --- a/pkg/cmd/inventory/update/update.auto.go +++ b/pkg/cmd/inventory/update/update.auto.go @@ -140,7 +140,7 @@ func (n *UpdateCmd) RunE(cmd *cobra.Command, args []string) error { inputIterators, flags.WithDataFlagValue(), flags.WithStringValue("newName", "name"), - cmdutil.WithTemplateValue(cfg), + cmdutil.WithTemplateValue(n.factory), flags.WithTemplateVariablesValue(), ) if err != nil { diff --git a/pkg/cmd/measurements/create/create.auto.go b/pkg/cmd/measurements/create/create.auto.go index c340b1555..6bf0cb25c 100644 --- a/pkg/cmd/measurements/create/create.auto.go +++ b/pkg/cmd/measurements/create/create.auto.go @@ -155,12 +155,12 @@ func (n *CreateCmd) RunE(cmd *cobra.Command, args []string) error { body, inputIterators, flags.WithDataFlagValue(), - c8yfetcher.WithDeviceByNameFirstMatch(client, args, "device", "source.id"), + c8yfetcher.WithDeviceByNameFirstMatch(n.factory, args, "device", "source.id"), flags.WithRelativeTimestamp("time", "time"), flags.WithStringValue("type", "type"), flags.WithDefaultTemplateString(` {time: _.Now('0s')}`), - cmdutil.WithTemplateValue(cfg), + cmdutil.WithTemplateValue(n.factory), flags.WithTemplateVariablesValue(), flags.WithRequiredProperties("type", "time", "source.id"), ) diff --git a/pkg/cmd/measurements/createBulk/createBulk.manual.go b/pkg/cmd/measurements/createBulk/createBulk.manual.go index c76c38d54..d01506926 100644 --- a/pkg/cmd/measurements/createBulk/createBulk.manual.go +++ b/pkg/cmd/measurements/createBulk/createBulk.manual.go @@ -154,11 +154,11 @@ func (n *CreateBulkCmd) RunE(cmd *cobra.Command, args []string) error { body, inputIterators, flags.WithDataFlagValue(), - c8yfetcher.WithDeviceByNameFirstMatch(client, args, "device", "source.id"), + c8yfetcher.WithDeviceByNameFirstMatch(n.factory, args, "device", "source.id"), flags.WithRelativeTimestamp("time", "time", ""), flags.WithStringValue("type", "type"), flags.WithDefaultTemplateString(`{time: _.Now('0s')} + (if std.isObject(input.value) then input.value else {source:{id:input.value}}) + {id:: '', 'self':: ''}`), - cmdutil.WithTemplateValue(cfg), + cmdutil.WithTemplateValue(n.factory), flags.WithTemplateVariablesValue(), flags.WithRequiredProperties("type", "time", "source.id"), ) diff --git a/pkg/cmd/measurements/deletecollection/deleteCollection.auto.go b/pkg/cmd/measurements/deletecollection/deleteCollection.auto.go index 2c26932c3..978436bef 100644 --- a/pkg/cmd/measurements/deletecollection/deleteCollection.auto.go +++ b/pkg/cmd/measurements/deletecollection/deleteCollection.auto.go @@ -100,7 +100,7 @@ func (n *DeleteCollectionCmd) RunE(cmd *cobra.Command, args []string) error { query, inputIterators, flags.WithCustomStringSlice(func() ([]string, error) { return cfg.GetQueryParameters(), nil }, "custom"), - c8yfetcher.WithDeviceByNameFirstMatch(client, args, "device", "source"), + c8yfetcher.WithDeviceByNameFirstMatch(n.factory, args, "device", "source"), flags.WithStringValue("type", "type"), flags.WithStringValue("fragmentType", "fragmentType"), flags.WithEncodedRelativeTimestamp("dateFrom", "dateFrom"), diff --git a/pkg/cmd/measurements/getseries/getSeries.auto.go b/pkg/cmd/measurements/getseries/getSeries.auto.go index afa236fd0..6d9a8653e 100644 --- a/pkg/cmd/measurements/getseries/getSeries.auto.go +++ b/pkg/cmd/measurements/getseries/getSeries.auto.go @@ -104,7 +104,7 @@ func (n *GetSeriesCmd) RunE(cmd *cobra.Command, args []string) error { query, inputIterators, flags.WithCustomStringSlice(func() ([]string, error) { return cfg.GetQueryParameters(), nil }, "custom"), - c8yfetcher.WithDeviceByNameFirstMatch(client, args, "device", "source"), + c8yfetcher.WithDeviceByNameFirstMatch(n.factory, args, "device", "source"), flags.WithStringSliceValues("series", "series", ""), flags.WithStringValue("aggregationType", "aggregationType"), flags.WithEncodedRelativeTimestamp("dateFrom", "dateFrom"), diff --git a/pkg/cmd/measurements/list/list.auto.go b/pkg/cmd/measurements/list/list.auto.go index adc805008..536653ed6 100644 --- a/pkg/cmd/measurements/list/list.auto.go +++ b/pkg/cmd/measurements/list/list.auto.go @@ -111,7 +111,7 @@ func (n *ListCmd) RunE(cmd *cobra.Command, args []string) error { query, inputIterators, flags.WithCustomStringSlice(func() ([]string, error) { return cfg.GetQueryParameters(), nil }, "custom"), - c8yfetcher.WithDeviceByNameFirstMatch(client, args, "device", "source"), + c8yfetcher.WithDeviceByNameFirstMatch(n.factory, args, "device", "source"), flags.WithStringValue("type", "type"), flags.WithStringValue("valueFragmentType", "valueFragmentType"), flags.WithStringValue("valueFragmentSeries", "valueFragmentSeries"), diff --git a/pkg/cmd/measurements/subscribe/subscribe.manual.go b/pkg/cmd/measurements/subscribe/subscribe.manual.go index d62f12b92..247f634f2 100644 --- a/pkg/cmd/measurements/subscribe/subscribe.manual.go +++ b/pkg/cmd/measurements/subscribe/subscribe.manual.go @@ -95,7 +95,7 @@ func (n *CmdSubscribe) RunE(cmd *cobra.Command, args []string) error { path, inputIterators, flags.WithStringDefaultValue("*", "device", "device"), - c8yfetcher.WithDeviceByNameFirstMatch(client, args, "device", "device"), + c8yfetcher.WithDeviceByNameFirstMatch(n.factory, args, "device", "device"), ) if err != nil { return err diff --git a/pkg/cmd/microservices/create/create.manual.go b/pkg/cmd/microservices/create/create.manual.go index 3b41d9ff5..88476a794 100644 --- a/pkg/cmd/microservices/create/create.manual.go +++ b/pkg/cmd/microservices/create/create.manual.go @@ -207,7 +207,7 @@ func (n *CmdCreate) RunE(cmd *cobra.Command, args []string) error { if applicationName != "" { - refs, err := c8yfetcher.FindMicroservices(client, []string{applicationName}, true, "") + refs, err := c8yfetcher.FindMicroservices(n.factory, []string{applicationName}, true, "") if err != nil { return cmderrors.NewUserError(err) diff --git a/pkg/cmd/microservices/createbinary/createBinary.auto.go b/pkg/cmd/microservices/createbinary/createBinary.auto.go index 759f6c3f0..52361ff98 100644 --- a/pkg/cmd/microservices/createbinary/createBinary.auto.go +++ b/pkg/cmd/microservices/createbinary/createBinary.auto.go @@ -135,7 +135,7 @@ func (n *CreateBinaryCmd) RunE(cmd *cobra.Command, args []string) error { cmd, formData, inputIterators, - flags.WithFormDataFileAndInfoWithTemplateSupport(cmdutil.NewTemplateResolver(cfg), "file", "data")..., + flags.WithFormDataFileAndInfoWithTemplateSupport(cmdutil.NewTemplateResolver(n.factory), "file", "data")..., ) if err != nil { return cmderrors.NewUserError(err) @@ -148,7 +148,7 @@ func (n *CreateBinaryCmd) RunE(cmd *cobra.Command, args []string) error { body, inputIterators, flags.WithDataFlagValue(), - cmdutil.WithTemplateValue(cfg), + cmdutil.WithTemplateValue(n.factory), flags.WithTemplateVariablesValue(), ) if err != nil { @@ -161,7 +161,7 @@ func (n *CreateBinaryCmd) RunE(cmd *cobra.Command, args []string) error { cmd, path, inputIterators, - c8yfetcher.WithMicroserviceByNameFirstMatch(client, args, "id", "id"), + c8yfetcher.WithMicroserviceByNameFirstMatch(n.factory, args, "id", "id"), ) if err != nil { return err diff --git a/pkg/cmd/microservices/delete/delete.auto.go b/pkg/cmd/microservices/delete/delete.auto.go index c85a081a4..40ad08ddd 100644 --- a/pkg/cmd/microservices/delete/delete.auto.go +++ b/pkg/cmd/microservices/delete/delete.auto.go @@ -149,7 +149,7 @@ func (n *DeleteCmd) RunE(cmd *cobra.Command, args []string) error { cmd, path, inputIterators, - c8yfetcher.WithMicroserviceByNameFirstMatch(client, args, "id", "id"), + c8yfetcher.WithMicroserviceByNameFirstMatch(n.factory, args, "id", "id"), ) if err != nil { return err diff --git a/pkg/cmd/microservices/disable/disable.auto.go b/pkg/cmd/microservices/disable/disable.auto.go index abce511d6..9fb9b5b20 100644 --- a/pkg/cmd/microservices/disable/disable.auto.go +++ b/pkg/cmd/microservices/disable/disable.auto.go @@ -153,8 +153,8 @@ func (n *DisableCmd) RunE(cmd *cobra.Command, args []string) error { cmd, path, inputIterators, - c8yfetcher.WithMicroserviceByNameFirstMatch(client, args, "id", "id"), - flags.WithStringDefaultValue(client.TenantName, "tenant", "tenant"), + c8yfetcher.WithMicroserviceByNameFirstMatch(n.factory, args, "id", "id"), + flags.WithStringDefaultValue(n.factory.GetTenant(), "tenant", "tenant"), ) if err != nil { return err diff --git a/pkg/cmd/microservices/enable/enable.auto.go b/pkg/cmd/microservices/enable/enable.auto.go index d95816365..61c8387c3 100644 --- a/pkg/cmd/microservices/enable/enable.auto.go +++ b/pkg/cmd/microservices/enable/enable.auto.go @@ -143,8 +143,8 @@ func (n *EnableCmd) RunE(cmd *cobra.Command, args []string) error { body, inputIterators, flags.WithDataFlagValue(), - c8yfetcher.WithMicroserviceByNameFirstMatch(client, args, "id", "application.id"), - cmdutil.WithTemplateValue(cfg), + c8yfetcher.WithMicroserviceByNameFirstMatch(n.factory, args, "id", "application.id"), + cmdutil.WithTemplateValue(n.factory), flags.WithTemplateVariablesValue(), ) if err != nil { @@ -157,7 +157,7 @@ func (n *EnableCmd) RunE(cmd *cobra.Command, args []string) error { cmd, path, inputIterators, - flags.WithStringDefaultValue(client.TenantName, "tenant", "tenant"), + flags.WithStringDefaultValue(n.factory.GetTenant(), "tenant", "tenant"), ) if err != nil { return err diff --git a/pkg/cmd/microservices/get/get.auto.go b/pkg/cmd/microservices/get/get.auto.go index 17f3fa31e..48b3b9776 100644 --- a/pkg/cmd/microservices/get/get.auto.go +++ b/pkg/cmd/microservices/get/get.auto.go @@ -150,7 +150,7 @@ func (n *GetCmd) RunE(cmd *cobra.Command, args []string) error { cmd, path, inputIterators, - c8yfetcher.WithMicroserviceByNameFirstMatch(client, args, "id", "id"), + c8yfetcher.WithMicroserviceByNameFirstMatch(n.factory, args, "id", "id"), ) if err != nil { return err diff --git a/pkg/cmd/microservices/getbootstrapuser/getBootstrapUser.auto.go b/pkg/cmd/microservices/getbootstrapuser/getBootstrapUser.auto.go index 8a0751448..5bb57c968 100644 --- a/pkg/cmd/microservices/getbootstrapuser/getBootstrapUser.auto.go +++ b/pkg/cmd/microservices/getbootstrapuser/getBootstrapUser.auto.go @@ -155,7 +155,7 @@ func (n *GetBootstrapUserCmd) RunE(cmd *cobra.Command, args []string) error { cmd, path, inputIterators, - c8yfetcher.WithMicroserviceByNameFirstMatch(client, args, "id", "id"), + c8yfetcher.WithMicroserviceByNameFirstMatch(n.factory, args, "id", "id"), ) if err != nil { return err diff --git a/pkg/cmd/microservices/getstatus/getStatus.auto.go b/pkg/cmd/microservices/getstatus/getStatus.auto.go index 013bef86f..4e7e00962 100644 --- a/pkg/cmd/microservices/getstatus/getStatus.auto.go +++ b/pkg/cmd/microservices/getstatus/getStatus.auto.go @@ -156,7 +156,7 @@ func (n *GetStatusCmd) RunE(cmd *cobra.Command, args []string) error { cmd, path, inputIterators, - c8yfetcher.WithMicroserviceByNameFirstMatch(client, args, "id", "id"), + c8yfetcher.WithMicroserviceByNameFirstMatch(n.factory, args, "id", "id"), ) if err != nil { return err diff --git a/pkg/cmd/microservices/list/list.auto.go b/pkg/cmd/microservices/list/list.auto.go index dfe87b791..96897b504 100644 --- a/pkg/cmd/microservices/list/list.auto.go +++ b/pkg/cmd/microservices/list/list.auto.go @@ -124,7 +124,7 @@ func (n *ListCmd) RunE(cmd *cobra.Command, args []string) error { flags.WithStringValue("owner", "owner"), flags.WithStringValue("providedFor", "providedFor"), flags.WithStringValue("subscriber", "subscriber"), - c8yfetcher.WithUserByNameFirstMatch(client, args, "user", "user"), + c8yfetcher.WithUserByNameFirstMatch(n.factory, args, "user", "user"), ) if err != nil { return cmderrors.NewUserError(err) diff --git a/pkg/cmd/microservices/loglevels/delete/delete.auto.go b/pkg/cmd/microservices/loglevels/delete/delete.auto.go index 20a5abf51..fd3708686 100644 --- a/pkg/cmd/microservices/loglevels/delete/delete.auto.go +++ b/pkg/cmd/microservices/loglevels/delete/delete.auto.go @@ -151,7 +151,7 @@ func (n *DeleteCmd) RunE(cmd *cobra.Command, args []string) error { flags.WithDefaultTemplateString(` {"configuredLevel":null} `), - cmdutil.WithTemplateValue(cfg), + cmdutil.WithTemplateValue(n.factory), flags.WithTemplateVariablesValue(), ) if err != nil { diff --git a/pkg/cmd/microservices/loglevels/set/set.auto.go b/pkg/cmd/microservices/loglevels/set/set.auto.go index b3a743634..32bd3530c 100644 --- a/pkg/cmd/microservices/loglevels/set/set.auto.go +++ b/pkg/cmd/microservices/loglevels/set/set.auto.go @@ -148,7 +148,7 @@ func (n *SetCmd) RunE(cmd *cobra.Command, args []string) error { inputIterators, flags.WithDataFlagValue(), flags.WithStringValue("logLevel", "configuredLevel"), - cmdutil.WithTemplateValue(cfg), + cmdutil.WithTemplateValue(n.factory), flags.WithTemplateVariablesValue(), flags.WithRequiredProperties("configuredLevel"), ) diff --git a/pkg/cmd/microservices/serviceuser/get/get.manual.go b/pkg/cmd/microservices/serviceuser/get/get.manual.go index e96cac3dd..60723a367 100644 --- a/pkg/cmd/microservices/serviceuser/get/get.manual.go +++ b/pkg/cmd/microservices/serviceuser/get/get.manual.go @@ -89,7 +89,7 @@ func (n *CmdGet) RunE(cmd *cobra.Command, args []string) error { cmd, path, inputIterators, - c8yfetcher.WithMicroserviceByNameFirstMatch(client, args, "id", "id"), + c8yfetcher.WithMicroserviceByNameFirstMatch(n.factory, args, "id", "id"), ) if err != nil { return err diff --git a/pkg/cmd/microservices/update/update.auto.go b/pkg/cmd/microservices/update/update.auto.go index 389b368c0..0cda141db 100644 --- a/pkg/cmd/microservices/update/update.auto.go +++ b/pkg/cmd/microservices/update/update.auto.go @@ -147,7 +147,7 @@ func (n *UpdateCmd) RunE(cmd *cobra.Command, args []string) error { flags.WithStringValue("availability", "availability"), flags.WithStringValue("contextPath", "contextPath"), flags.WithStringValue("resourcesUrl", "resourcesUrl"), - cmdutil.WithTemplateValue(cfg), + cmdutil.WithTemplateValue(n.factory), flags.WithTemplateVariablesValue(), ) if err != nil { @@ -160,7 +160,7 @@ func (n *UpdateCmd) RunE(cmd *cobra.Command, args []string) error { cmd, path, inputIterators, - c8yfetcher.WithMicroserviceByNameFirstMatch(client, args, "id", "id"), + c8yfetcher.WithMicroserviceByNameFirstMatch(n.factory, args, "id", "id"), ) if err != nil { return err diff --git a/pkg/cmd/notification2/subscriptions/create/create.auto.go b/pkg/cmd/notification2/subscriptions/create/create.auto.go index b3cf35e54..1e08bb760 100644 --- a/pkg/cmd/notification2/subscriptions/create/create.auto.go +++ b/pkg/cmd/notification2/subscriptions/create/create.auto.go @@ -151,13 +151,13 @@ func (n *CreateCmd) RunE(cmd *cobra.Command, args []string) error { body, inputIterators, flags.WithDataFlagValue(), - c8yfetcher.WithDeviceByNameFirstMatch(client, args, "device", "source.id"), + c8yfetcher.WithDeviceByNameFirstMatch(n.factory, args, "device", "source.id"), flags.WithStringValue("name", "subscription"), flags.WithStringValue("context", "context"), flags.WithStringSliceValues("fragmentsToCopy", "fragmentsToCopy", ""), flags.WithStringSliceValues("apiFilter", "subscriptionFilter.apis", ""), flags.WithStringValue("typeFilter", "subscriptionFilter.typeFilter"), - cmdutil.WithTemplateValue(cfg), + cmdutil.WithTemplateValue(n.factory), flags.WithTemplateVariablesValue(), flags.WithRequiredProperties("context", "subscription"), ) diff --git a/pkg/cmd/notification2/subscriptions/deletebysource/deleteBySource.auto.go b/pkg/cmd/notification2/subscriptions/deletebysource/deleteBySource.auto.go index 93c4b603e..a17dfca4f 100644 --- a/pkg/cmd/notification2/subscriptions/deletebysource/deleteBySource.auto.go +++ b/pkg/cmd/notification2/subscriptions/deletebysource/deleteBySource.auto.go @@ -96,7 +96,7 @@ func (n *DeleteBySourceCmd) RunE(cmd *cobra.Command, args []string) error { query, inputIterators, flags.WithCustomStringSlice(func() ([]string, error) { return cfg.GetQueryParameters(), nil }, "custom"), - c8yfetcher.WithDeviceByNameFirstMatch(client, args, "device", "source"), + c8yfetcher.WithDeviceByNameFirstMatch(n.factory, args, "device", "source"), flags.WithStringValue("context", "context"), ) if err != nil { diff --git a/pkg/cmd/notification2/subscriptions/list/list.auto.go b/pkg/cmd/notification2/subscriptions/list/list.auto.go index 20c3b3e79..fd5a87f29 100644 --- a/pkg/cmd/notification2/subscriptions/list/list.auto.go +++ b/pkg/cmd/notification2/subscriptions/list/list.auto.go @@ -104,7 +104,7 @@ func (n *ListCmd) RunE(cmd *cobra.Command, args []string) error { query, inputIterators, flags.WithCustomStringSlice(func() ([]string, error) { return cfg.GetQueryParameters(), nil }, "custom"), - c8yfetcher.WithDeviceByNameFirstMatch(client, args, "device", "source"), + c8yfetcher.WithDeviceByNameFirstMatch(n.factory, args, "device", "source"), flags.WithStringValue("context", "context"), ) if err != nil { diff --git a/pkg/cmd/notification2/tokens/create/create.auto.go b/pkg/cmd/notification2/tokens/create/create.auto.go index 75be82a38..404a31f2d 100644 --- a/pkg/cmd/notification2/tokens/create/create.auto.go +++ b/pkg/cmd/notification2/tokens/create/create.auto.go @@ -147,7 +147,7 @@ func (n *CreateCmd) RunE(cmd *cobra.Command, args []string) error { flags.WithStringValue("name", "subscription"), flags.WithIntValue("expiresInMinutes", "expiresInMinutes"), flags.WithStringValue("shared", "shared"), - cmdutil.WithTemplateValue(cfg), + cmdutil.WithTemplateValue(n.factory), flags.WithTemplateVariablesValue(), flags.WithRequiredProperties("subscriber", "subscription"), ) diff --git a/pkg/cmd/operations/create/create.auto.go b/pkg/cmd/operations/create/create.auto.go index 2cbee5c68..066cd2f46 100644 --- a/pkg/cmd/operations/create/create.auto.go +++ b/pkg/cmd/operations/create/create.auto.go @@ -138,9 +138,9 @@ func (n *CreateCmd) RunE(cmd *cobra.Command, args []string) error { body, inputIterators, flags.WithDataFlagValue(), - c8yfetcher.WithDeviceByNameFirstMatch(client, args, "device", "deviceId"), + c8yfetcher.WithDeviceByNameFirstMatch(n.factory, args, "device", "deviceId"), flags.WithStringValue("description", "description"), - cmdutil.WithTemplateValue(cfg), + cmdutil.WithTemplateValue(n.factory), flags.WithTemplateVariablesValue(), flags.WithRequiredProperties("deviceId"), ) diff --git a/pkg/cmd/operations/deletecollection/deleteCollection.auto.go b/pkg/cmd/operations/deletecollection/deleteCollection.auto.go index 1b37991ad..a37c74432 100644 --- a/pkg/cmd/operations/deletecollection/deleteCollection.auto.go +++ b/pkg/cmd/operations/deletecollection/deleteCollection.auto.go @@ -106,8 +106,8 @@ func (n *DeleteCollectionCmd) RunE(cmd *cobra.Command, args []string) error { query, inputIterators, flags.WithCustomStringSlice(func() ([]string, error) { return cfg.GetQueryParameters(), nil }, "custom"), - c8yfetcher.WithDeviceByNameFirstMatch(client, args, "agent", "agentId"), - c8yfetcher.WithDeviceByNameFirstMatch(client, args, "device", "deviceId"), + c8yfetcher.WithDeviceByNameFirstMatch(n.factory, args, "agent", "agentId"), + c8yfetcher.WithDeviceByNameFirstMatch(n.factory, args, "device", "deviceId"), flags.WithEncodedRelativeTimestamp("dateFrom", "dateFrom"), flags.WithEncodedRelativeTimestamp("dateTo", "dateTo"), flags.WithStringValue("status", "status"), diff --git a/pkg/cmd/operations/list/list.auto.go b/pkg/cmd/operations/list/list.auto.go index 763878f5e..257ffa73e 100644 --- a/pkg/cmd/operations/list/list.auto.go +++ b/pkg/cmd/operations/list/list.auto.go @@ -114,8 +114,8 @@ func (n *ListCmd) RunE(cmd *cobra.Command, args []string) error { query, inputIterators, flags.WithCustomStringSlice(func() ([]string, error) { return cfg.GetQueryParameters(), nil }, "custom"), - c8yfetcher.WithDeviceByNameFirstMatch(client, args, "agent", "agentId"), - c8yfetcher.WithDeviceByNameFirstMatch(client, args, "device", "deviceId"), + c8yfetcher.WithDeviceByNameFirstMatch(n.factory, args, "agent", "agentId"), + c8yfetcher.WithDeviceByNameFirstMatch(n.factory, args, "device", "deviceId"), flags.WithStringValue("fragmentType", "fragmentType"), flags.WithEncodedRelativeTimestamp("dateFrom", "dateFrom"), flags.WithEncodedRelativeTimestamp("dateTo", "dateTo"), diff --git a/pkg/cmd/operations/subscribe/subscribe.manual.go b/pkg/cmd/operations/subscribe/subscribe.manual.go index 39e8c8255..2f4f92e4c 100644 --- a/pkg/cmd/operations/subscribe/subscribe.manual.go +++ b/pkg/cmd/operations/subscribe/subscribe.manual.go @@ -93,7 +93,7 @@ func (n *CmdSubscribe) RunE(cmd *cobra.Command, args []string) error { path, inputIterators, flags.WithStringDefaultValue("*", "device", "device"), - c8yfetcher.WithDeviceByNameFirstMatch(client, args, "device", "device"), + c8yfetcher.WithDeviceByNameFirstMatch(n.factory, args, "device", "device"), ) if err != nil { return err diff --git a/pkg/cmd/operations/update/update.auto.go b/pkg/cmd/operations/update/update.auto.go index 1b9301a91..64031bbf9 100644 --- a/pkg/cmd/operations/update/update.auto.go +++ b/pkg/cmd/operations/update/update.auto.go @@ -141,7 +141,7 @@ func (n *UpdateCmd) RunE(cmd *cobra.Command, args []string) error { flags.WithDataFlagValue(), flags.WithStringValue("status", "status"), flags.WithStringValue("failureReason", "failureReason"), - cmdutil.WithTemplateValue(cfg), + cmdutil.WithTemplateValue(n.factory), flags.WithTemplateVariablesValue(), flags.WithRequiredProperties("status"), ) diff --git a/pkg/cmd/realtime/subscribeall/subscribeall.manual.go b/pkg/cmd/realtime/subscribeall/subscribeall.manual.go index 5356b19cd..352237e08 100644 --- a/pkg/cmd/realtime/subscribeall/subscribeall.manual.go +++ b/pkg/cmd/realtime/subscribeall/subscribeall.manual.go @@ -87,7 +87,7 @@ func (n *CmdSubscribeAll) RunE(cmd *cobra.Command, args []string) error { path, inputIterators, flags.WithStringDefaultValue("*", "device", "device"), - c8yfetcher.WithDeviceByNameFirstMatch(client, args, "device", "device"), + c8yfetcher.WithDeviceByNameFirstMatch(n.factory, args, "device", "device"), ) if err != nil { return err diff --git a/pkg/cmd/retentionrules/create/create.auto.go b/pkg/cmd/retentionrules/create/create.auto.go index 1339f2b45..92702e7dd 100644 --- a/pkg/cmd/retentionrules/create/create.auto.go +++ b/pkg/cmd/retentionrules/create/create.auto.go @@ -150,7 +150,7 @@ func (n *CreateCmd) RunE(cmd *cobra.Command, args []string) error { flags.WithBoolValue("editable", "editable", ""), flags.WithDefaultTemplateString(` {maximumAge: 365}`), - cmdutil.WithTemplateValue(cfg), + cmdutil.WithTemplateValue(n.factory), flags.WithTemplateVariablesValue(), flags.WithRequiredProperties("maximumAge", "dataType"), ) diff --git a/pkg/cmd/retentionrules/update/update.auto.go b/pkg/cmd/retentionrules/update/update.auto.go index e6e99161c..cb6167f23 100644 --- a/pkg/cmd/retentionrules/update/update.auto.go +++ b/pkg/cmd/retentionrules/update/update.auto.go @@ -149,7 +149,7 @@ func (n *UpdateCmd) RunE(cmd *cobra.Command, args []string) error { flags.WithStringValue("source", "source"), flags.WithIntValue("maximumAge", "maximumAge"), flags.WithBoolValue("editable", "editable", ""), - cmdutil.WithTemplateValue(cfg), + cmdutil.WithTemplateValue(n.factory), flags.WithTemplateVariablesValue(), ) if err != nil { diff --git a/pkg/cmd/root/root.go b/pkg/cmd/root/root.go index 3b416918b..69c28ab8e 100644 --- a/pkg/cmd/root/root.go +++ b/pkg/cmd/root/root.go @@ -2,7 +2,9 @@ package root import ( "fmt" + "io/fs" "os" + "path/filepath" "strings" "sync" @@ -47,6 +49,7 @@ import ( eventsCmd "github.com/reubenmiller/go-c8y-cli/v2/pkg/cmd/events" eventsAssertCmd "github.com/reubenmiller/go-c8y-cli/v2/pkg/cmd/events/assert" eventsSubscribeCmd "github.com/reubenmiller/go-c8y-cli/v2/pkg/cmd/events/subscribe" + "github.com/reubenmiller/go-c8y-cli/v2/pkg/cmd/extension" "github.com/reubenmiller/go-c8y-cli/v2/pkg/cmd/factory" firmwareCmd "github.com/reubenmiller/go-c8y-cli/v2/pkg/cmd/firmware" firmwareVersionsPatchesCmd "github.com/reubenmiller/go-c8y-cli/v2/pkg/cmd/firmware/patches" @@ -98,10 +101,12 @@ import ( utilCmd "github.com/reubenmiller/go-c8y-cli/v2/pkg/cmd/util" versionCmd "github.com/reubenmiller/go-c8y-cli/v2/pkg/cmd/version" "github.com/reubenmiller/go-c8y-cli/v2/pkg/cmderrors" + "github.com/reubenmiller/go-c8y-cli/v2/pkg/cmdparser" "github.com/reubenmiller/go-c8y-cli/v2/pkg/cmdutil" "github.com/reubenmiller/go-c8y-cli/v2/pkg/completion" "github.com/reubenmiller/go-c8y-cli/v2/pkg/config" "github.com/reubenmiller/go-c8y-cli/v2/pkg/dataview" + "github.com/reubenmiller/go-c8y-cli/v2/pkg/extensions" "github.com/reubenmiller/go-c8y-cli/v2/pkg/flags" "github.com/reubenmiller/go-c8y-cli/v2/pkg/logger" "github.com/reubenmiller/go-c8y-cli/v2/pkg/utilities" @@ -152,7 +157,23 @@ func NewCmdRoot(f *cmdutil.Factory, version, buildDate string) *CmdRoot { `), PersistentPreRunE: func(cmd *cobra.Command, args []string) error { disableEncryptionCheck := !cmdutil.IsConfigEncryptionCheckEnabled(cmd) - if err := ccmd.Configure(disableEncryptionCheck); err != nil { + + // Note: Support setting verbose/debug mode even when flag parsing + // is disabled so that logging still works as expected. Extensions make + // use of this + forceVerbose := false + forceDebug := false + if cmd.DisableFlagParsing { + for _, v := range args { + if v == "-v" || v == "-v=true" || v == "--verbose" || v == "--verbose=true" { + forceVerbose = true + } + if v == "--debug" || v == "--debug=true" { + forceDebug = true + } + } + } + if err := ccmd.Configure(disableEncryptionCheck, forceVerbose, forceDebug); err != nil { return err } if notice := flags.GetDeprecationNoticeFromAnnotation(cmd); notice != "" { @@ -257,6 +278,7 @@ func NewCmdRoot(f *cmdutil.Factory, version, buildDate string) *CmdRoot { config.OutputTable.String()+"\ttable format", config.OutputCSV.String()+"\tcsv format without headers", config.OutputCSVWithHeader.String()+"\tcsv format with headers", + config.OutputTSV.String()+"\ttab delimited format", config.OutputServerResponse.String()+"\tUnparsed server response", ), completion.WithSessionFile("session", config.ConfigExtensions, func() string { @@ -299,12 +321,11 @@ func NewCmdRoot(f *cmdutil.Factory, version, buildDate string) *CmdRoot { currenttenantCmd.NewSubCommand(f).GetCommand(), currentuserCmd.NewSubCommand(f).GetCommand(), activityLogCmd.NewSubCommand(f).GetCommand(), + extension.NewCmdExtension(f), } cmd.AddCommand(commands...) - // todo: merge custom commands - // alarms := alarmsCmd.NewSubCommand(f).GetCommand() alarms.AddCommand(alarmsSubscribeCmd.NewCmdSubscribe(f).GetCommand()) alarms.AddCommand(alarmsAssertCmd.NewSubCommand(f).GetCommand()) @@ -422,14 +443,178 @@ func NewCmdRoot(f *cmdutil.Factory, version, buildDate string) *CmdRoot { cmd.AddCommand(aliasCmd.NewCmdAlias(f)) cmd.AddCommand(apiCmd.NewSubCommand(f).GetCommand()) - // Handle errors (not in cobra libary) + // Add sub commands for the extensions + extensions := f.ExtensionManager().List() + if err := ConvertToCobraCommands(f, cmd, extensions); err != nil { + if log, logErr := f.Logger(); logErr == nil { + log.Warnf("Errors while loading some extensions. Functionality may be reduced. %s", err) + } + } + + // Handle errors (not in cobra library) cmd.SilenceErrors = true ccmd.Command = cmd return ccmd } -func (c *CmdRoot) Configure(disableEncryptionCheck bool) error { +func isTabCompletionCommand() bool { + return strings.HasPrefix(strings.Join(os.Args[1:], ""), "__complete") +} + +func ConvertToCobraCommands(f *cmdutil.Factory, cmd *cobra.Command, extensions []extensions.Extension) error { + extCommandTree := make(map[string]*cobra.Command) + // Enable flag parsing when using tab completion, otherwise disable it + // as it affects passing the arguments to the extension binary + disableFlagParsing := !isTabCompletionCommand() + _ = disableFlagParsing + + log, err := f.Logger() + if err != nil { + return err + } + + var extError error + for _, ext := range extensions { + commands, err := ext.Commands() + if err != nil { + extError = fmt.Errorf("%w", err) + continue + } + + extName := ext.Name() + extRoot := &cobra.Command{ + Use: extName, + Short: extName + " extension", + } + extCommandTree[extName] = extRoot + + // + // API/Spec commands + // + // Parse all api files + apiDir := filepath.Join(ext.Path(), "api") + if _, err := os.Stat(apiDir); !os.IsNotExist(err) { + walkErr := filepath.WalkDir(apiDir, func(path string, d fs.DirEntry, walkErr error) error { + if walkErr != nil { + return walkErr + } + + if d.IsDir() { + return nil + } + + if ext := filepath.Ext(path); ext != ".yaml" && ext != ".yml" { + return nil + } + + log.Debugf("Reading extension file: %s", path) + spec, err := os.Open(path) + if err != nil { + return err + } + defer spec.Close() + extCommand, err := cmdparser.ParseCommand(spec, f, cmd.Root()) + if err != nil { + // Only log a warning for the user, don't prevent the whole cli from working + log.Warnf("Invalid extension file. reason=%s. file=%s", err, path) + // return fmt.Errorf("%w. file=%s", err, path) + } else { + if extCommand != nil { + extRoot.AddCommand(extCommand) + } + } + + return nil + }) + if walkErr != nil { + return fmt.Errorf("%w. extension_name=%s", walkErr, ext.Name()) + } + } + + // + // Shell commands + // + for _, command := range commands { + path := extName + " " + command.Name() + key := path + name := path + var parentCmd *cobra.Command + exists := false + + parts := strings.Split(path, " ") + + if len(parts) > 1 { + name = parts[len(parts)-1] + key = strings.Join(parts[0:len(parts)-1], " ") + parentName := parts[0] + if len(parts) > 2 { + parentName = parts[len(parts)-2] + } + parentCmd, exists = extCommandTree[key] + if !exists { + parentCmd = &cobra.Command{ + Use: parentName, + Short: fmt.Sprintf("%s command group", parentName), + } + if len(parts) == 3 { + extRoot.AddCommand(parentCmd) + } + extCommandTree[key] = parentCmd + } + + iCmd := &cobra.Command{ + Use: name, + Short: fmt.Sprintf("Run %s command", name), + FParseErrWhitelist: cobra.FParseErrWhitelist{UnknownFlags: true}, + DisableFlagParsing: disableFlagParsing, + RunE: func(name, exe string) func(*cobra.Command, []string) error { + return func(cmd *cobra.Command, args []string) error { + log, err := f.Logger() + if err != nil { + return err + } + log.Infof("Executing extension. name: %s, command: %s, args: %v", name, exe, args) + _, err = f.ExtensionManager().Execute(exe, args, false, f.IOStreams.In, f.IOStreams.Out, f.IOStreams.ErrOut) + return err + } + }(extName, command.Command()), + } + + if parentCmd != nil { + parentCmd.AddCommand(iCmd) + } + } else { + iCmd := &cobra.Command{ + Use: key, + Short: fmt.Sprintf("%s command group", key), + FParseErrWhitelist: cobra.FParseErrWhitelist{UnknownFlags: true}, + DisableFlagParsing: disableFlagParsing, + RunE: func(name, exe string) func(*cobra.Command, []string) error { + return func(cmd *cobra.Command, args []string) error { + log, err := f.Logger() + if err != nil { + return err + } + log.Infof("Executing extension. name: %s, command: %s, args: %v", name, exe, args) + _, err = f.ExtensionManager().Execute(exe, args, false, f.IOStreams.In, f.IOStreams.Out, f.IOStreams.ErrOut) + return err + } + }(extName, command.Command()), + } + extRoot.AddCommand(iCmd) + } + } + + // Only add if the is at least 1 command + if len(extRoot.Commands()) > 0 { + cmd.AddCommand(extRoot) + } + } + return extError +} + +func (c *CmdRoot) Configure(disableEncryptionCheck, forceVerbose, forceDebug bool) error { cfg, err := c.Factory.Config() if err != nil { return err @@ -489,10 +674,10 @@ func (c *CmdRoot) Configure(disableEncryptionCheck bool) error { // mode errors logOptions.Silent = false } else { - if cfg.Verbose() { + if cfg.Verbose() || forceVerbose { logOptions.Level = zapcore.InfoLevel } - if cfg.Debug() { + if cfg.Debug() || forceDebug { logOptions.Level = zapcore.DebugLevel } } @@ -525,7 +710,17 @@ func (c *CmdRoot) Configure(disableEncryptionCheck bool) error { } l, _ := c.Factory.Logger() - dv, err := dataview.NewDataView(".*", ".json", l, cfg.GetViewPaths()...) + viewPaths := cfg.GetViewPaths() + + // Add extensions + for _, ext := range c.Factory.ExtensionManager().List() { + path := ext.ViewPath() + if path != "" { + viewPaths = append(viewPaths, cmdutil.BuildTemplatePath(ext.Name(), path)) + } + } + + dv, err := dataview.NewDataView(".*", ".json", l, viewPaths...) c.dataview = dv return dv, err } diff --git a/pkg/cmd/settings/update/update.manual.go b/pkg/cmd/settings/update/update.manual.go index ae67658ed..91d529847 100644 --- a/pkg/cmd/settings/update/update.manual.go +++ b/pkg/cmd/settings/update/update.manual.go @@ -375,6 +375,9 @@ var updateSettingsOptions = map[string]argumentHandler{ "25", "30", }, nil, cobra.ShellCompDirectiveNoFileComp}, + + // extensions + "extensions.datadir": {"extensions.datadir", "string", config.SettingsExtensionDataDir, []string{}, nil, cobra.ShellCompDirectiveDefault}, } // NewCmdUpdate returns a new command used to update session settings diff --git a/pkg/cmd/smartgroups/create/create.auto.go b/pkg/cmd/smartgroups/create/create.auto.go index 11404a149..a41dbc9f2 100644 --- a/pkg/cmd/smartgroups/create/create.auto.go +++ b/pkg/cmd/smartgroups/create/create.auto.go @@ -149,7 +149,7 @@ func (n *CreateCmd) RunE(cmd *cobra.Command, args []string) error { flags.WithBoolValue("invisible", "c8y_IsDynamicGroup.invisible", "{}"), flags.WithRequiredTemplateString(` {type: 'c8y_DynamicGroup', c8y_DeviceQueryString: '', c8y_IsDynamicGroup: {}}`), - cmdutil.WithTemplateValue(cfg), + cmdutil.WithTemplateValue(n.factory), flags.WithTemplateVariablesValue(), flags.WithRequiredProperties("name", "c8y_DeviceQueryString"), ) diff --git a/pkg/cmd/smartgroups/delete/delete.auto.go b/pkg/cmd/smartgroups/delete/delete.auto.go index 7ce58d456..4df8ce576 100644 --- a/pkg/cmd/smartgroups/delete/delete.auto.go +++ b/pkg/cmd/smartgroups/delete/delete.auto.go @@ -147,7 +147,7 @@ func (n *DeleteCmd) RunE(cmd *cobra.Command, args []string) error { cmd, path, inputIterators, - c8yfetcher.WithSmartGroupByNameFirstMatch(client, args, "id", "id"), + c8yfetcher.WithSmartGroupByNameFirstMatch(n.factory, args, "id", "id"), ) if err != nil { return err diff --git a/pkg/cmd/smartgroups/get/get.auto.go b/pkg/cmd/smartgroups/get/get.auto.go index 6a6b262b5..0ace77ef6 100644 --- a/pkg/cmd/smartgroups/get/get.auto.go +++ b/pkg/cmd/smartgroups/get/get.auto.go @@ -163,7 +163,7 @@ func (n *GetCmd) RunE(cmd *cobra.Command, args []string) error { cmd, path, inputIterators, - c8yfetcher.WithSmartGroupByNameFirstMatch(client, args, "id", "id"), + c8yfetcher.WithSmartGroupByNameFirstMatch(n.factory, args, "id", "id"), ) if err != nil { return err diff --git a/pkg/cmd/smartgroups/update/update.auto.go b/pkg/cmd/smartgroups/update/update.auto.go index 6ee55d13f..3907fc794 100644 --- a/pkg/cmd/smartgroups/update/update.auto.go +++ b/pkg/cmd/smartgroups/update/update.auto.go @@ -141,7 +141,7 @@ func (n *UpdateCmd) RunE(cmd *cobra.Command, args []string) error { flags.WithDataFlagValue(), flags.WithStringValue("newName", "name"), flags.WithStringValue("query", "c8y_DeviceQueryString"), - cmdutil.WithTemplateValue(cfg), + cmdutil.WithTemplateValue(n.factory), flags.WithTemplateVariablesValue(), ) if err != nil { @@ -154,7 +154,7 @@ func (n *UpdateCmd) RunE(cmd *cobra.Command, args []string) error { cmd, path, inputIterators, - c8yfetcher.WithSmartGroupByNameFirstMatch(client, args, "id", "id"), + c8yfetcher.WithSmartGroupByNameFirstMatch(n.factory, args, "id", "id"), ) if err != nil { return err diff --git a/pkg/cmd/software/create/create.auto.go b/pkg/cmd/software/create/create.auto.go index 30c404ad8..f9878b688 100644 --- a/pkg/cmd/software/create/create.auto.go +++ b/pkg/cmd/software/create/create.auto.go @@ -151,7 +151,7 @@ func (n *CreateCmd) RunE(cmd *cobra.Command, args []string) error { flags.WithStringValue("deviceType", "c8y_Filter.type"), flags.WithDefaultTemplateString(` {type: 'c8y_Software', c8y_Global:{}}`), - cmdutil.WithTemplateValue(cfg), + cmdutil.WithTemplateValue(n.factory), flags.WithTemplateVariablesValue(), flags.WithRequiredProperties("type", "name"), ) diff --git a/pkg/cmd/software/delete/delete.auto.go b/pkg/cmd/software/delete/delete.auto.go index 1128d9850..f45902094 100644 --- a/pkg/cmd/software/delete/delete.auto.go +++ b/pkg/cmd/software/delete/delete.auto.go @@ -150,7 +150,7 @@ func (n *DeleteCmd) RunE(cmd *cobra.Command, args []string) error { cmd, path, inputIterators, - c8yfetcher.WithSoftwareByNameFirstMatch(client, args, "id", "id"), + c8yfetcher.WithSoftwareByNameFirstMatch(n.factory, args, "id", "id"), ) if err != nil { return err diff --git a/pkg/cmd/software/get/get.auto.go b/pkg/cmd/software/get/get.auto.go index e51b253dd..a5a3f5821 100644 --- a/pkg/cmd/software/get/get.auto.go +++ b/pkg/cmd/software/get/get.auto.go @@ -159,7 +159,7 @@ func (n *GetCmd) RunE(cmd *cobra.Command, args []string) error { cmd, path, inputIterators, - c8yfetcher.WithSoftwareByNameFirstMatch(client, args, "id", "id"), + c8yfetcher.WithSoftwareByNameFirstMatch(n.factory, args, "id", "id"), ) if err != nil { return err diff --git a/pkg/cmd/software/update/update.auto.go b/pkg/cmd/software/update/update.auto.go index aa2e58682..c9d929119 100644 --- a/pkg/cmd/software/update/update.auto.go +++ b/pkg/cmd/software/update/update.auto.go @@ -142,7 +142,7 @@ func (n *UpdateCmd) RunE(cmd *cobra.Command, args []string) error { flags.WithStringValue("newName", "name"), flags.WithStringValue("description", "description"), flags.WithStringValue("deviceType", "c8y_Filter.type"), - cmdutil.WithTemplateValue(cfg), + cmdutil.WithTemplateValue(n.factory), flags.WithTemplateVariablesValue(), ) if err != nil { @@ -155,7 +155,7 @@ func (n *UpdateCmd) RunE(cmd *cobra.Command, args []string) error { cmd, path, inputIterators, - c8yfetcher.WithSoftwareByNameFirstMatch(client, args, "id", "id"), + c8yfetcher.WithSoftwareByNameFirstMatch(n.factory, args, "id", "id"), ) if err != nil { return err diff --git a/pkg/cmd/software/versions/create/create.manual.go b/pkg/cmd/software/versions/create/create.manual.go index efbdb7525..f52802baf 100644 --- a/pkg/cmd/software/versions/create/create.manual.go +++ b/pkg/cmd/software/versions/create/create.manual.go @@ -144,7 +144,7 @@ func (n *CreateCmd) RunE(cmd *cobra.Command, args []string) error { flags.WithStringValue("url", "c8y_Software.url"), flags.WithDefaultTemplateString(` {type: 'c8y_SoftwareBinary', c8y_Global:{}}`), - cmdutil.WithTemplateValue(cfg), + cmdutil.WithTemplateValue(n.factory), flags.WithTemplateVariablesValue(), flags.WithRequiredProperties("type", "c8y_Software.version"), ) @@ -158,7 +158,7 @@ func (n *CreateCmd) RunE(cmd *cobra.Command, args []string) error { cmd, path, inputIterators, - c8yfetcher.WithSoftwareByNameFirstMatch(client, args, "software", "software"), + c8yfetcher.WithSoftwareByNameFirstMatch(n.factory, args, "software", "software"), ) if err != nil { return err diff --git a/pkg/cmd/software/versions/delete/delete.auto.go b/pkg/cmd/software/versions/delete/delete.auto.go index ed0b9fa8e..0863064ad 100644 --- a/pkg/cmd/software/versions/delete/delete.auto.go +++ b/pkg/cmd/software/versions/delete/delete.auto.go @@ -152,8 +152,8 @@ func (n *DeleteCmd) RunE(cmd *cobra.Command, args []string) error { cmd, path, inputIterators, - c8yfetcher.WithSoftwareVersionByNameFirstMatch(client, args, "id", "id"), - c8yfetcher.WithSoftwareByNameFirstMatch(client, args, "software", "software"), + c8yfetcher.WithSoftwareVersionByNameFirstMatch(n.factory, "software", args, "id", "id"), + c8yfetcher.WithSoftwareByNameFirstMatch(n.factory, args, "software", "software"), ) if err != nil { return err diff --git a/pkg/cmd/software/versions/get/get.auto.go b/pkg/cmd/software/versions/get/get.auto.go index 693c9a604..9ced5bb15 100644 --- a/pkg/cmd/software/versions/get/get.auto.go +++ b/pkg/cmd/software/versions/get/get.auto.go @@ -161,8 +161,8 @@ func (n *GetCmd) RunE(cmd *cobra.Command, args []string) error { cmd, path, inputIterators, - c8yfetcher.WithSoftwareVersionByNameFirstMatch(client, args, "id", "id"), - c8yfetcher.WithSoftwareByNameFirstMatch(client, args, "software", "software"), + c8yfetcher.WithSoftwareVersionByNameFirstMatch(n.factory, "software", args, "id", "id"), + c8yfetcher.WithSoftwareByNameFirstMatch(n.factory, args, "software", "software"), ) if err != nil { return err diff --git a/pkg/cmd/software/versions/install/install.auto.go b/pkg/cmd/software/versions/install/install.auto.go index aeeeeb45e..18ba11c15 100644 --- a/pkg/cmd/software/versions/install/install.auto.go +++ b/pkg/cmd/software/versions/install/install.auto.go @@ -152,14 +152,14 @@ func (n *InstallCmd) RunE(cmd *cobra.Command, args []string) error { body, inputIterators, flags.WithDataFlagValue(), - c8yfetcher.WithDeviceByNameFirstMatch(client, args, "device", "deviceId"), + c8yfetcher.WithDeviceByNameFirstMatch(n.factory, args, "device", "deviceId"), flags.WithStringValue("software", "c8y_SoftwareUpdate.0.name"), flags.WithStringValue("version", "c8y_SoftwareUpdate.0.version"), flags.WithStringValue("url", "c8y_SoftwareUpdate.0.url"), flags.WithStringValue("description", "description"), - c8yfetcher.WithSoftwareVersionData(client, "software", "version", "url", args, "", "c8y_SoftwareUpdate.0"), + c8yfetcher.WithSoftwareVersionData(n.factory, "software", "version", "url", args, "", "c8y_SoftwareUpdate.0"), flags.WithStringValue("action", "c8y_SoftwareUpdate.0.action"), - cmdutil.WithTemplateValue(cfg), + cmdutil.WithTemplateValue(n.factory), flags.WithTemplateVariablesValue(), flags.WithRequiredProperties("deviceId", "c8y_SoftwareUpdate.0.name", "c8y_SoftwareUpdate.0.version", "c8y_SoftwareUpdate.0.action"), ) diff --git a/pkg/cmd/software/versions/list/list.auto.go b/pkg/cmd/software/versions/list/list.auto.go index c1d4b591f..0a3aa5a77 100644 --- a/pkg/cmd/software/versions/list/list.auto.go +++ b/pkg/cmd/software/versions/list/list.auto.go @@ -117,7 +117,7 @@ func (n *ListCmd) RunE(cmd *cobra.Command, args []string) error { flags.WithCumulocityQuery( []flags.GetOption{ flags.WithStringValue("query", "query", "%s"), - c8yfetcher.WithSoftwareByNameFirstMatch(client, args, "software", "software", "bygroupid(%s)"), + c8yfetcher.WithSoftwareByNameFirstMatch(n.factory, args, "software", "software", "bygroupid(%s)"), flags.WithStaticStringValue("ignorePatches", "not(has(c8y_Patch))"), flags.WithStringValue("version", "version", "(c8y_Software.version eq '%s')"), flags.WithStringValue("url", "url", "(c8y_Software.url eq '%s')"), diff --git a/pkg/cmd/software/versions/uninstall/uninstall.auto.go b/pkg/cmd/software/versions/uninstall/uninstall.auto.go index ccad2b5a6..016af2967 100644 --- a/pkg/cmd/software/versions/uninstall/uninstall.auto.go +++ b/pkg/cmd/software/versions/uninstall/uninstall.auto.go @@ -144,11 +144,11 @@ func (n *UninstallCmd) RunE(cmd *cobra.Command, args []string) error { body, inputIterators, flags.WithDataFlagValue(), - c8yfetcher.WithDeviceByNameFirstMatch(client, args, "device", "deviceId"), + c8yfetcher.WithDeviceByNameFirstMatch(n.factory, args, "device", "deviceId"), flags.WithStringValue("software", "c8y_SoftwareUpdate.0.name"), flags.WithStringValue("version", "c8y_SoftwareUpdate.0.version"), flags.WithStringValue("action", "c8y_SoftwareUpdate.0.action"), - cmdutil.WithTemplateValue(cfg), + cmdutil.WithTemplateValue(n.factory), flags.WithTemplateVariablesValue(), flags.WithRequiredProperties("deviceId", "c8y_SoftwareUpdate.0.name", "c8y_SoftwareUpdate.0.action"), ) diff --git a/pkg/cmd/template/execute/execute.manual.go b/pkg/cmd/template/execute/execute.manual.go index 33aa3d68f..13d6850d6 100644 --- a/pkg/cmd/template/execute/execute.manual.go +++ b/pkg/cmd/template/execute/execute.manual.go @@ -109,7 +109,7 @@ func (n *CmdExecute) newTemplate(cmd *cobra.Command, args []string) error { inputIterators, flags.WithOverrideValue("input", "input"), flags.WithDataFlagValue(), - cmdutil.WithTemplateValue(cfg), + cmdutil.WithTemplateValue(n.factory), flags.WithTemplateVariablesValue(), flags.WithStringValue("input", "input", ""), ) diff --git a/pkg/cmd/tenantoptions/create/create.auto.go b/pkg/cmd/tenantoptions/create/create.auto.go index 8766a17a0..21824eb6d 100644 --- a/pkg/cmd/tenantoptions/create/create.auto.go +++ b/pkg/cmd/tenantoptions/create/create.auto.go @@ -142,7 +142,7 @@ func (n *CreateCmd) RunE(cmd *cobra.Command, args []string) error { flags.WithStringValue("category", "category"), flags.WithStringValue("key", "key"), flags.WithStringValue("value", "value"), - cmdutil.WithTemplateValue(cfg), + cmdutil.WithTemplateValue(n.factory), flags.WithTemplateVariablesValue(), flags.WithRequiredProperties("category", "key", "value"), ) diff --git a/pkg/cmd/tenantoptions/update/update.auto.go b/pkg/cmd/tenantoptions/update/update.auto.go index 2917048e5..e2158c120 100644 --- a/pkg/cmd/tenantoptions/update/update.auto.go +++ b/pkg/cmd/tenantoptions/update/update.auto.go @@ -140,7 +140,7 @@ func (n *UpdateCmd) RunE(cmd *cobra.Command, args []string) error { inputIterators, flags.WithDataFlagValue(), flags.WithStringValue("value", "value"), - cmdutil.WithTemplateValue(cfg), + cmdutil.WithTemplateValue(n.factory), flags.WithTemplateVariablesValue(), flags.WithRequiredProperties("value"), ) diff --git a/pkg/cmd/tenantoptions/updatebulk/updateBulk.auto.go b/pkg/cmd/tenantoptions/updatebulk/updateBulk.auto.go index a147c0957..d2abaac42 100644 --- a/pkg/cmd/tenantoptions/updatebulk/updateBulk.auto.go +++ b/pkg/cmd/tenantoptions/updatebulk/updateBulk.auto.go @@ -136,7 +136,7 @@ func (n *UpdateBulkCmd) RunE(cmd *cobra.Command, args []string) error { body, inputIterators, flags.WithDataFlagValue(), - cmdutil.WithTemplateValue(cfg), + cmdutil.WithTemplateValue(n.factory), flags.WithTemplateVariablesValue(), ) if err != nil { diff --git a/pkg/cmd/tenantoptions/updateedit/updateEdit.auto.go b/pkg/cmd/tenantoptions/updateedit/updateEdit.auto.go index 9cc485c61..ab9d5930c 100644 --- a/pkg/cmd/tenantoptions/updateedit/updateEdit.auto.go +++ b/pkg/cmd/tenantoptions/updateedit/updateEdit.auto.go @@ -143,7 +143,7 @@ func (n *UpdateEditCmd) RunE(cmd *cobra.Command, args []string) error { inputIterators, flags.WithDataFlagValue(), flags.WithStringValue("editable", "editable"), - cmdutil.WithTemplateValue(cfg), + cmdutil.WithTemplateValue(n.factory), flags.WithTemplateVariablesValue(), ) if err != nil { diff --git a/pkg/cmd/tenants/create/create.auto.go b/pkg/cmd/tenants/create/create.auto.go index e8b51fbfa..235f0f009 100644 --- a/pkg/cmd/tenants/create/create.auto.go +++ b/pkg/cmd/tenants/create/create.auto.go @@ -148,7 +148,7 @@ func (n *CreateCmd) RunE(cmd *cobra.Command, args []string) error { flags.WithStringValue("contactName", "contactName"), flags.WithStringValue("contactPhone", "contact_phone"), flags.WithStringValue("tenantId", "tenantId"), - cmdutil.WithTemplateValue(cfg), + cmdutil.WithTemplateValue(n.factory), flags.WithTemplateVariablesValue(), flags.WithRequiredProperties("company", "domain"), ) diff --git a/pkg/cmd/tenants/delete/delete.auto.go b/pkg/cmd/tenants/delete/delete.auto.go index 87c6984d2..717382dc6 100644 --- a/pkg/cmd/tenants/delete/delete.auto.go +++ b/pkg/cmd/tenants/delete/delete.auto.go @@ -145,7 +145,7 @@ func (n *DeleteCmd) RunE(cmd *cobra.Command, args []string) error { cmd, path, inputIterators, - flags.WithStringDefaultValue(client.TenantName, "id", "id"), + flags.WithStringDefaultValue(n.factory.GetTenant(), "id", "id"), ) if err != nil { return err diff --git a/pkg/cmd/tenants/disableapplication/disableApplication.auto.go b/pkg/cmd/tenants/disableapplication/disableApplication.auto.go index c00684b18..f1565f6d5 100644 --- a/pkg/cmd/tenants/disableapplication/disableApplication.auto.go +++ b/pkg/cmd/tenants/disableapplication/disableApplication.auto.go @@ -149,8 +149,8 @@ func (n *DisableApplicationCmd) RunE(cmd *cobra.Command, args []string) error { cmd, path, inputIterators, - flags.WithStringDefaultValue(client.TenantName, "tenant", "tenant"), - c8yfetcher.WithApplicationByNameFirstMatch(client, args, "application", "application"), + flags.WithStringDefaultValue(n.factory.GetTenant(), "tenant", "tenant"), + c8yfetcher.WithApplicationByNameFirstMatch(n.factory, args, "application", "application"), ) if err != nil { return err diff --git a/pkg/cmd/tenants/enableapplication/enableApplication.auto.go b/pkg/cmd/tenants/enableapplication/enableApplication.auto.go index 35b5bcf65..713397f86 100644 --- a/pkg/cmd/tenants/enableapplication/enableApplication.auto.go +++ b/pkg/cmd/tenants/enableapplication/enableApplication.auto.go @@ -139,8 +139,8 @@ func (n *EnableApplicationCmd) RunE(cmd *cobra.Command, args []string) error { body, inputIterators, flags.WithDataFlagValue(), - c8yfetcher.WithApplicationByNameFirstMatch(client, args, "application", "application.id"), - cmdutil.WithTemplateValue(cfg), + c8yfetcher.WithApplicationByNameFirstMatch(n.factory, args, "application", "application.id"), + cmdutil.WithTemplateValue(n.factory), flags.WithTemplateVariablesValue(), ) if err != nil { @@ -153,7 +153,7 @@ func (n *EnableApplicationCmd) RunE(cmd *cobra.Command, args []string) error { cmd, path, inputIterators, - flags.WithStringDefaultValue(client.TenantName, "tenant", "tenant"), + flags.WithStringDefaultValue(n.factory.GetTenant(), "tenant", "tenant"), ) if err != nil { return err diff --git a/pkg/cmd/tenants/get/get.auto.go b/pkg/cmd/tenants/get/get.auto.go index 5fd40b0bb..d654381dd 100644 --- a/pkg/cmd/tenants/get/get.auto.go +++ b/pkg/cmd/tenants/get/get.auto.go @@ -149,7 +149,7 @@ func (n *GetCmd) RunE(cmd *cobra.Command, args []string) error { cmd, path, inputIterators, - flags.WithStringDefaultValue(client.TenantName, "id", "id"), + flags.WithStringDefaultValue(n.factory.GetTenant(), "id", "id"), ) if err != nil { return err diff --git a/pkg/cmd/tenants/listreferences/listReferences.auto.go b/pkg/cmd/tenants/listreferences/listReferences.auto.go index 09276ddd0..867505bea 100644 --- a/pkg/cmd/tenants/listreferences/listReferences.auto.go +++ b/pkg/cmd/tenants/listreferences/listReferences.auto.go @@ -151,7 +151,7 @@ func (n *ListReferencesCmd) RunE(cmd *cobra.Command, args []string) error { cmd, path, inputIterators, - flags.WithStringDefaultValue(client.TenantName, "tenant", "tenant"), + flags.WithStringDefaultValue(n.factory.GetTenant(), "tenant", "tenant"), ) if err != nil { return err diff --git a/pkg/cmd/tenants/update/update.auto.go b/pkg/cmd/tenants/update/update.auto.go index d60ba4f74..44ca07e22 100644 --- a/pkg/cmd/tenants/update/update.auto.go +++ b/pkg/cmd/tenants/update/update.auto.go @@ -148,7 +148,7 @@ func (n *UpdateCmd) RunE(cmd *cobra.Command, args []string) error { flags.WithStringValue("adminPass", "adminPass"), flags.WithStringValue("contactName", "contactName"), flags.WithStringValue("contactPhone", "contact_phone"), - cmdutil.WithTemplateValue(cfg), + cmdutil.WithTemplateValue(n.factory), flags.WithTemplateVariablesValue(), ) if err != nil { @@ -161,7 +161,7 @@ func (n *UpdateCmd) RunE(cmd *cobra.Command, args []string) error { cmd, path, inputIterators, - flags.WithStringDefaultValue(client.TenantName, "id", "id"), + flags.WithStringDefaultValue(n.factory.GetTenant(), "id", "id"), ) if err != nil { return err diff --git a/pkg/cmd/usergroups/create/create.auto.go b/pkg/cmd/usergroups/create/create.auto.go index 1ffefb769..574c14b0a 100644 --- a/pkg/cmd/usergroups/create/create.auto.go +++ b/pkg/cmd/usergroups/create/create.auto.go @@ -141,7 +141,7 @@ func (n *CreateCmd) RunE(cmd *cobra.Command, args []string) error { flags.WithDataFlagValue(), flags.WithStringValue("name", "name"), flags.WithStringSliceValues("deviceProperties", "deviceProperties", ""), - cmdutil.WithTemplateValue(cfg), + cmdutil.WithTemplateValue(n.factory), flags.WithTemplateVariablesValue(), ) if err != nil { @@ -154,7 +154,7 @@ func (n *CreateCmd) RunE(cmd *cobra.Command, args []string) error { cmd, path, inputIterators, - flags.WithStringDefaultValue(client.TenantName, "tenant", "tenant"), + flags.WithStringDefaultValue(n.factory.GetTenant(), "tenant", "tenant"), ) if err != nil { return err diff --git a/pkg/cmd/usergroups/delete/delete.auto.go b/pkg/cmd/usergroups/delete/delete.auto.go index 37d7d0712..c9b61b2d2 100644 --- a/pkg/cmd/usergroups/delete/delete.auto.go +++ b/pkg/cmd/usergroups/delete/delete.auto.go @@ -149,8 +149,8 @@ func (n *DeleteCmd) RunE(cmd *cobra.Command, args []string) error { cmd, path, inputIterators, - flags.WithStringDefaultValue(client.TenantName, "tenant", "tenant"), - c8yfetcher.WithUserGroupByNameFirstMatch(client, args, "id", "id"), + flags.WithStringDefaultValue(n.factory.GetTenant(), "tenant", "tenant"), + c8yfetcher.WithUserGroupByNameFirstMatch(n.factory, args, "id", "id"), ) if err != nil { return err diff --git a/pkg/cmd/usergroups/get/get.auto.go b/pkg/cmd/usergroups/get/get.auto.go index 87ee691ff..1e687afe2 100644 --- a/pkg/cmd/usergroups/get/get.auto.go +++ b/pkg/cmd/usergroups/get/get.auto.go @@ -153,8 +153,8 @@ func (n *GetCmd) RunE(cmd *cobra.Command, args []string) error { cmd, path, inputIterators, - flags.WithStringDefaultValue(client.TenantName, "tenant", "tenant"), - c8yfetcher.WithUserGroupByNameFirstMatch(client, args, "id", "id"), + flags.WithStringDefaultValue(n.factory.GetTenant(), "tenant", "tenant"), + c8yfetcher.WithUserGroupByNameFirstMatch(n.factory, args, "id", "id"), ) if err != nil { return err diff --git a/pkg/cmd/usergroups/getbyname/getByName.auto.go b/pkg/cmd/usergroups/getbyname/getByName.auto.go index 35f2df898..97165ffd2 100644 --- a/pkg/cmd/usergroups/getbyname/getByName.auto.go +++ b/pkg/cmd/usergroups/getbyname/getByName.auto.go @@ -150,7 +150,7 @@ func (n *GetByNameCmd) RunE(cmd *cobra.Command, args []string) error { cmd, path, inputIterators, - flags.WithStringDefaultValue(client.TenantName, "tenant", "tenant"), + flags.WithStringDefaultValue(n.factory.GetTenant(), "tenant", "tenant"), flags.WithStringValue("name", "name"), ) if err != nil { diff --git a/pkg/cmd/usergroups/list/list.auto.go b/pkg/cmd/usergroups/list/list.auto.go index d40c9ac8a..cd905c4d9 100644 --- a/pkg/cmd/usergroups/list/list.auto.go +++ b/pkg/cmd/usergroups/list/list.auto.go @@ -151,7 +151,7 @@ func (n *ListCmd) RunE(cmd *cobra.Command, args []string) error { cmd, path, inputIterators, - flags.WithStringDefaultValue(client.TenantName, "tenant", "tenant"), + flags.WithStringDefaultValue(n.factory.GetTenant(), "tenant", "tenant"), ) if err != nil { return err diff --git a/pkg/cmd/usergroups/update/update.auto.go b/pkg/cmd/usergroups/update/update.auto.go index c1a517b95..957a9e6b9 100644 --- a/pkg/cmd/usergroups/update/update.auto.go +++ b/pkg/cmd/usergroups/update/update.auto.go @@ -145,7 +145,7 @@ func (n *UpdateCmd) RunE(cmd *cobra.Command, args []string) error { inputIterators, flags.WithDataFlagValue(), flags.WithStringValue("name", "name"), - cmdutil.WithTemplateValue(cfg), + cmdutil.WithTemplateValue(n.factory), flags.WithTemplateVariablesValue(), ) if err != nil { @@ -158,8 +158,8 @@ func (n *UpdateCmd) RunE(cmd *cobra.Command, args []string) error { cmd, path, inputIterators, - flags.WithStringDefaultValue(client.TenantName, "tenant", "tenant"), - c8yfetcher.WithUserGroupByNameFirstMatch(client, args, "id", "id"), + flags.WithStringDefaultValue(n.factory.GetTenant(), "tenant", "tenant"), + c8yfetcher.WithUserGroupByNameFirstMatch(n.factory, args, "id", "id"), ) if err != nil { return err diff --git a/pkg/cmd/userreferences/addusertogroup/addUserToGroup.auto.go b/pkg/cmd/userreferences/addusertogroup/addUserToGroup.auto.go index 1d06e721e..d175d9e15 100644 --- a/pkg/cmd/userreferences/addusertogroup/addUserToGroup.auto.go +++ b/pkg/cmd/userreferences/addusertogroup/addUserToGroup.auto.go @@ -148,8 +148,8 @@ func (n *AddUserToGroupCmd) RunE(cmd *cobra.Command, args []string) error { body, inputIterators, flags.WithDataFlagValue(), - c8yfetcher.WithUserSelfByNameFirstMatch(client, args, "user", "user.self"), - cmdutil.WithTemplateValue(cfg), + c8yfetcher.WithUserSelfByNameFirstMatch(n.factory, args, "user", "user.self"), + cmdutil.WithTemplateValue(n.factory), flags.WithTemplateVariablesValue(), flags.WithRequiredProperties("user.self"), ) @@ -163,8 +163,8 @@ func (n *AddUserToGroupCmd) RunE(cmd *cobra.Command, args []string) error { cmd, path, inputIterators, - c8yfetcher.WithUserGroupByNameFirstMatch(client, args, "group", "group"), - flags.WithStringDefaultValue(client.TenantName, "tenant", "tenant"), + c8yfetcher.WithUserGroupByNameFirstMatch(n.factory, args, "group", "group"), + flags.WithStringDefaultValue(n.factory.GetTenant(), "tenant", "tenant"), ) if err != nil { return err diff --git a/pkg/cmd/userreferences/deleteuserfromgroup/deleteUserFromGroup.auto.go b/pkg/cmd/userreferences/deleteuserfromgroup/deleteUserFromGroup.auto.go index 2e3cad140..6633d5c19 100644 --- a/pkg/cmd/userreferences/deleteuserfromgroup/deleteUserFromGroup.auto.go +++ b/pkg/cmd/userreferences/deleteuserfromgroup/deleteUserFromGroup.auto.go @@ -155,9 +155,9 @@ func (n *DeleteUserFromGroupCmd) RunE(cmd *cobra.Command, args []string) error { cmd, path, inputIterators, - c8yfetcher.WithUserGroupByNameFirstMatch(client, args, "group", "group"), - c8yfetcher.WithUserByNameFirstMatch(client, args, "user", "user"), - flags.WithStringDefaultValue(client.TenantName, "tenant", "tenant"), + c8yfetcher.WithUserGroupByNameFirstMatch(n.factory, args, "group", "group"), + c8yfetcher.WithUserByNameFirstMatch(n.factory, args, "user", "user"), + flags.WithStringDefaultValue(n.factory.GetTenant(), "tenant", "tenant"), ) if err != nil { return err diff --git a/pkg/cmd/userreferences/listgroupmembership/listGroupMembership.auto.go b/pkg/cmd/userreferences/listgroupmembership/listGroupMembership.auto.go index fb7c657fc..9dc872d95 100644 --- a/pkg/cmd/userreferences/listgroupmembership/listGroupMembership.auto.go +++ b/pkg/cmd/userreferences/listgroupmembership/listGroupMembership.auto.go @@ -158,8 +158,8 @@ func (n *ListGroupMembershipCmd) RunE(cmd *cobra.Command, args []string) error { cmd, path, inputIterators, - c8yfetcher.WithUserGroupByNameFirstMatch(client, args, "id", "id"), - flags.WithStringDefaultValue(client.TenantName, "tenant", "tenant"), + c8yfetcher.WithUserGroupByNameFirstMatch(n.factory, args, "id", "id"), + flags.WithStringDefaultValue(n.factory.GetTenant(), "tenant", "tenant"), ) if err != nil { return err diff --git a/pkg/cmd/userroles/addroletogroup/addRoleToGroup.auto.go b/pkg/cmd/userroles/addroletogroup/addRoleToGroup.auto.go index 40eb22d0f..6936afc35 100644 --- a/pkg/cmd/userroles/addroletogroup/addRoleToGroup.auto.go +++ b/pkg/cmd/userroles/addroletogroup/addRoleToGroup.auto.go @@ -143,8 +143,8 @@ func (n *AddRoleToGroupCmd) RunE(cmd *cobra.Command, args []string) error { body, inputIterators, flags.WithDataFlagValue(), - c8yfetcher.WithRoleSelfByNameFirstMatch(client, args, "role", "role.self"), - cmdutil.WithTemplateValue(cfg), + c8yfetcher.WithRoleSelfByNameFirstMatch(n.factory, args, "role", "role.self"), + cmdutil.WithTemplateValue(n.factory), flags.WithTemplateVariablesValue(), ) if err != nil { @@ -157,8 +157,8 @@ func (n *AddRoleToGroupCmd) RunE(cmd *cobra.Command, args []string) error { cmd, path, inputIterators, - flags.WithStringDefaultValue(client.TenantName, "tenant", "tenant"), - c8yfetcher.WithUserGroupByNameFirstMatch(client, args, "group", "group"), + flags.WithStringDefaultValue(n.factory.GetTenant(), "tenant", "tenant"), + c8yfetcher.WithUserGroupByNameFirstMatch(n.factory, args, "group", "group"), ) if err != nil { return err diff --git a/pkg/cmd/userroles/addroletouser/addRoleToUser.auto.go b/pkg/cmd/userroles/addroletouser/addRoleToUser.auto.go index ccf07ec49..caaa388a0 100644 --- a/pkg/cmd/userroles/addroletouser/addRoleToUser.auto.go +++ b/pkg/cmd/userroles/addroletouser/addRoleToUser.auto.go @@ -142,8 +142,8 @@ func (n *AddRoleToUserCmd) RunE(cmd *cobra.Command, args []string) error { body, inputIterators, flags.WithDataFlagValue(), - c8yfetcher.WithRoleSelfByNameFirstMatch(client, args, "role", "role.self"), - cmdutil.WithTemplateValue(cfg), + c8yfetcher.WithRoleSelfByNameFirstMatch(n.factory, args, "role", "role.self"), + cmdutil.WithTemplateValue(n.factory), flags.WithTemplateVariablesValue(), ) if err != nil { @@ -156,8 +156,8 @@ func (n *AddRoleToUserCmd) RunE(cmd *cobra.Command, args []string) error { cmd, path, inputIterators, - flags.WithStringDefaultValue(client.TenantName, "tenant", "tenant"), - c8yfetcher.WithUserByNameFirstMatch(client, args, "user", "user"), + flags.WithStringDefaultValue(n.factory.GetTenant(), "tenant", "tenant"), + c8yfetcher.WithUserByNameFirstMatch(n.factory, args, "user", "user"), ) if err != nil { return err diff --git a/pkg/cmd/userroles/deleterolefromgroup/deleteRoleFromGroup.auto.go b/pkg/cmd/userroles/deleterolefromgroup/deleteRoleFromGroup.auto.go index d83ee8901..1eaea2eda 100644 --- a/pkg/cmd/userroles/deleterolefromgroup/deleteRoleFromGroup.auto.go +++ b/pkg/cmd/userroles/deleterolefromgroup/deleteRoleFromGroup.auto.go @@ -153,9 +153,9 @@ func (n *DeleteRoleFromGroupCmd) RunE(cmd *cobra.Command, args []string) error { cmd, path, inputIterators, - c8yfetcher.WithUserGroupByNameFirstMatch(client, args, "group", "group"), - c8yfetcher.WithRoleByNameFirstMatch(client, args, "role", "role"), - flags.WithStringDefaultValue(client.TenantName, "tenant", "tenant"), + c8yfetcher.WithUserGroupByNameFirstMatch(n.factory, args, "group", "group"), + c8yfetcher.WithRoleByNameFirstMatch(n.factory, args, "role", "role"), + flags.WithStringDefaultValue(n.factory.GetTenant(), "tenant", "tenant"), ) if err != nil { return err diff --git a/pkg/cmd/userroles/deleterolefromuser/deleteRoleFromUser.auto.go b/pkg/cmd/userroles/deleterolefromuser/deleteRoleFromUser.auto.go index 65b2b25e4..05873bd1a 100644 --- a/pkg/cmd/userroles/deleterolefromuser/deleteRoleFromUser.auto.go +++ b/pkg/cmd/userroles/deleterolefromuser/deleteRoleFromUser.auto.go @@ -152,9 +152,9 @@ func (n *DeleteRoleFromUserCmd) RunE(cmd *cobra.Command, args []string) error { cmd, path, inputIterators, - c8yfetcher.WithUserByNameFirstMatch(client, args, "user", "user"), - c8yfetcher.WithRoleByNameFirstMatch(client, args, "role", "role"), - flags.WithStringDefaultValue(client.TenantName, "tenant", "tenant"), + c8yfetcher.WithUserByNameFirstMatch(n.factory, args, "user", "user"), + c8yfetcher.WithRoleByNameFirstMatch(n.factory, args, "role", "role"), + flags.WithStringDefaultValue(n.factory.GetTenant(), "tenant", "tenant"), ) if err != nil { return err diff --git a/pkg/cmd/userroles/getrolereferencecollectionfromgroup/getRoleReferenceCollectionFromGroup.auto.go b/pkg/cmd/userroles/getrolereferencecollectionfromgroup/getRoleReferenceCollectionFromGroup.auto.go index e61412adf..ff91a5bce 100644 --- a/pkg/cmd/userroles/getrolereferencecollectionfromgroup/getRoleReferenceCollectionFromGroup.auto.go +++ b/pkg/cmd/userroles/getrolereferencecollectionfromgroup/getRoleReferenceCollectionFromGroup.auto.go @@ -155,8 +155,8 @@ func (n *GetRoleReferenceCollectionFromGroupCmd) RunE(cmd *cobra.Command, args [ cmd, path, inputIterators, - flags.WithStringDefaultValue(client.TenantName, "tenant", "tenant"), - c8yfetcher.WithUserGroupByNameFirstMatch(client, args, "group", "group"), + flags.WithStringDefaultValue(n.factory.GetTenant(), "tenant", "tenant"), + c8yfetcher.WithUserGroupByNameFirstMatch(n.factory, args, "group", "group"), ) if err != nil { return err diff --git a/pkg/cmd/userroles/getrolereferencecollectionfromuser/getRoleReferenceCollectionFromUser.auto.go b/pkg/cmd/userroles/getrolereferencecollectionfromuser/getRoleReferenceCollectionFromUser.auto.go index 7b2c3c681..5c147bd78 100644 --- a/pkg/cmd/userroles/getrolereferencecollectionfromuser/getRoleReferenceCollectionFromUser.auto.go +++ b/pkg/cmd/userroles/getrolereferencecollectionfromuser/getRoleReferenceCollectionFromUser.auto.go @@ -154,8 +154,8 @@ func (n *GetRoleReferenceCollectionFromUserCmd) RunE(cmd *cobra.Command, args [] cmd, path, inputIterators, - flags.WithStringDefaultValue(client.TenantName, "tenant", "tenant"), - c8yfetcher.WithUserByNameFirstMatch(client, args, "user", "user"), + flags.WithStringDefaultValue(n.factory.GetTenant(), "tenant", "tenant"), + c8yfetcher.WithUserByNameFirstMatch(n.factory, args, "user", "user"), ) if err != nil { return err diff --git a/pkg/cmd/users/create/create.auto.go b/pkg/cmd/users/create/create.auto.go index 9cb02caa5..d79d9a9f7 100644 --- a/pkg/cmd/users/create/create.auto.go +++ b/pkg/cmd/users/create/create.auto.go @@ -159,7 +159,7 @@ func (n *CreateCmd) RunE(cmd *cobra.Command, args []string) error { flags.WithStringValue("password", "password"), flags.WithBoolValue("sendPasswordResetEmail", "sendPasswordResetEmail", ""), flags.WithDataValue("customProperties", "customProperties"), - cmdutil.WithTemplateValue(cfg), + cmdutil.WithTemplateValue(n.factory), flags.WithTemplateVariablesValue(), flags.WithRequiredProperties("userName"), ) @@ -173,7 +173,7 @@ func (n *CreateCmd) RunE(cmd *cobra.Command, args []string) error { cmd, path, inputIterators, - flags.WithStringDefaultValue(client.TenantName, "tenant", "tenant"), + flags.WithStringDefaultValue(n.factory.GetTenant(), "tenant", "tenant"), ) if err != nil { return err diff --git a/pkg/cmd/users/delete/delete.auto.go b/pkg/cmd/users/delete/delete.auto.go index 7e4812526..f7f406540 100644 --- a/pkg/cmd/users/delete/delete.auto.go +++ b/pkg/cmd/users/delete/delete.auto.go @@ -150,8 +150,8 @@ func (n *DeleteCmd) RunE(cmd *cobra.Command, args []string) error { cmd, path, inputIterators, - flags.WithStringDefaultValue(client.TenantName, "tenant", "tenant"), - c8yfetcher.WithUserByNameFirstMatch(client, args, "id", "id"), + flags.WithStringDefaultValue(n.factory.GetTenant(), "tenant", "tenant"), + c8yfetcher.WithUserByNameFirstMatch(n.factory, args, "id", "id"), ) if err != nil { return err diff --git a/pkg/cmd/users/get/get.auto.go b/pkg/cmd/users/get/get.auto.go index 044394ce9..3b0a883da 100644 --- a/pkg/cmd/users/get/get.auto.go +++ b/pkg/cmd/users/get/get.auto.go @@ -152,8 +152,8 @@ func (n *GetCmd) RunE(cmd *cobra.Command, args []string) error { cmd, path, inputIterators, - c8yfetcher.WithUserByNameFirstMatch(client, args, "id", "id"), - flags.WithStringDefaultValue(client.TenantName, "tenant", "tenant"), + c8yfetcher.WithUserByNameFirstMatch(n.factory, args, "id", "id"), + flags.WithStringDefaultValue(n.factory.GetTenant(), "tenant", "tenant"), ) if err != nil { return err diff --git a/pkg/cmd/users/getuserbyname/getUserByName.auto.go b/pkg/cmd/users/getuserbyname/getUserByName.auto.go index ea696f3fa..799db6551 100644 --- a/pkg/cmd/users/getuserbyname/getUserByName.auto.go +++ b/pkg/cmd/users/getuserbyname/getUserByName.auto.go @@ -150,7 +150,7 @@ func (n *GetUserByNameCmd) RunE(cmd *cobra.Command, args []string) error { cmd, path, inputIterators, - flags.WithStringDefaultValue(client.TenantName, "tenant", "tenant"), + flags.WithStringDefaultValue(n.factory.GetTenant(), "tenant", "tenant"), flags.WithStringValue("name", "name"), ) if err != nil { diff --git a/pkg/cmd/users/list/list.auto.go b/pkg/cmd/users/list/list.auto.go index da2b2a421..8916fea20 100644 --- a/pkg/cmd/users/list/list.auto.go +++ b/pkg/cmd/users/list/list.auto.go @@ -161,7 +161,7 @@ func (n *ListCmd) RunE(cmd *cobra.Command, args []string) error { cmd, path, inputIterators, - flags.WithStringDefaultValue(client.TenantName, "tenant", "tenant"), + flags.WithStringDefaultValue(n.factory.GetTenant(), "tenant", "tenant"), ) if err != nil { return err diff --git a/pkg/cmd/users/listusermembership/listUserMembership.auto.go b/pkg/cmd/users/listusermembership/listUserMembership.auto.go index 5a42c7a1e..a131bf467 100644 --- a/pkg/cmd/users/listusermembership/listUserMembership.auto.go +++ b/pkg/cmd/users/listusermembership/listUserMembership.auto.go @@ -154,8 +154,8 @@ func (n *ListUserMembershipCmd) RunE(cmd *cobra.Command, args []string) error { cmd, path, inputIterators, - c8yfetcher.WithUserByNameFirstMatch(client, args, "id", "id"), - flags.WithStringDefaultValue(client.TenantName, "tenant", "tenant"), + c8yfetcher.WithUserByNameFirstMatch(n.factory, args, "id", "id"), + flags.WithStringDefaultValue(n.factory.GetTenant(), "tenant", "tenant"), ) if err != nil { return err diff --git a/pkg/cmd/users/resetuserpassword/resetUserPassword.auto.go b/pkg/cmd/users/resetuserpassword/resetUserPassword.auto.go index 79b784871..af5ccf0e1 100644 --- a/pkg/cmd/users/resetuserpassword/resetUserPassword.auto.go +++ b/pkg/cmd/users/resetuserpassword/resetUserPassword.auto.go @@ -142,7 +142,7 @@ func (n *ResetUserPasswordCmd) RunE(cmd *cobra.Command, args []string) error { flags.WithStringValue("newPassword", "password"), flags.WithRequiredTemplateString(` {sendPasswordResetEmail: !std.objectHas(self, 'password')}`), - cmdutil.WithTemplateValue(cfg), + cmdutil.WithTemplateValue(n.factory), flags.WithTemplateVariablesValue(), ) if err != nil { @@ -155,8 +155,8 @@ func (n *ResetUserPasswordCmd) RunE(cmd *cobra.Command, args []string) error { cmd, path, inputIterators, - c8yfetcher.WithUserByNameFirstMatch(client, args, "id", "id"), - flags.WithStringDefaultValue(client.TenantName, "tenant", "tenant"), + c8yfetcher.WithUserByNameFirstMatch(n.factory, args, "id", "id"), + flags.WithStringDefaultValue(n.factory.GetTenant(), "tenant", "tenant"), ) if err != nil { return err diff --git a/pkg/cmd/users/revoketotpsecret/revokeTOTPSecret.auto.go b/pkg/cmd/users/revoketotpsecret/revokeTOTPSecret.auto.go index e829a9c0f..e1a139961 100644 --- a/pkg/cmd/users/revoketotpsecret/revokeTOTPSecret.auto.go +++ b/pkg/cmd/users/revoketotpsecret/revokeTOTPSecret.auto.go @@ -151,8 +151,8 @@ func (n *RevokeTOTPSecretCmd) RunE(cmd *cobra.Command, args []string) error { cmd, path, inputIterators, - flags.WithStringDefaultValue(client.TenantName, "tenant", "tenant"), - c8yfetcher.WithUserByNameFirstMatch(client, args, "id", "id"), + flags.WithStringDefaultValue(n.factory.GetTenant(), "tenant", "tenant"), + c8yfetcher.WithUserByNameFirstMatch(n.factory, args, "id", "id"), ) if err != nil { return err diff --git a/pkg/cmd/users/update/update.auto.go b/pkg/cmd/users/update/update.auto.go index fe02b2979..40a74307b 100644 --- a/pkg/cmd/users/update/update.auto.go +++ b/pkg/cmd/users/update/update.auto.go @@ -159,7 +159,7 @@ func (n *UpdateCmd) RunE(cmd *cobra.Command, args []string) error { flags.WithStringValue("password", "password"), flags.WithBoolValue("sendPasswordResetEmail", "sendPasswordResetEmail", ""), flags.WithDataValue("customProperties", "customProperties"), - cmdutil.WithTemplateValue(cfg), + cmdutil.WithTemplateValue(n.factory), flags.WithTemplateVariablesValue(), ) if err != nil { @@ -172,8 +172,8 @@ func (n *UpdateCmd) RunE(cmd *cobra.Command, args []string) error { cmd, path, inputIterators, - c8yfetcher.WithUserByNameFirstMatch(client, args, "id", "id"), - flags.WithStringDefaultValue(client.TenantName, "tenant", "tenant"), + c8yfetcher.WithUserByNameFirstMatch(n.factory, args, "id", "id"), + flags.WithStringDefaultValue(n.factory.GetTenant(), "tenant", "tenant"), ) if err != nil { return err diff --git a/pkg/cmderrors/cmderrors.go b/pkg/cmderrors/cmderrors.go index 2473af34b..e73f0a257 100644 --- a/pkg/cmderrors/cmderrors.go +++ b/pkg/cmderrors/cmderrors.go @@ -136,6 +136,13 @@ var ( OperationCount AssertionErrorContext = "operationCount" ) +func NewSilentError() error { + return CommandError{ + silent: true, + ExitCode: ExitError, + } +} + func NewAssertionError(e *AssertionError) error { return CommandError{ silent: false, diff --git a/pkg/cmdparser/cmdparser.go b/pkg/cmdparser/cmdparser.go new file mode 100644 index 000000000..bebed15d5 --- /dev/null +++ b/pkg/cmdparser/cmdparser.go @@ -0,0 +1,740 @@ +package cmdparser + +import ( + "fmt" + "io" + "os" + "strconv" + + "github.com/reubenmiller/go-c8y-cli/v2/internal/integration/models" + "github.com/reubenmiller/go-c8y-cli/v2/pkg/c8ybinary" + "github.com/reubenmiller/go-c8y-cli/v2/pkg/c8yfetcher" + "github.com/reubenmiller/go-c8y-cli/v2/pkg/cmdutil" + "github.com/reubenmiller/go-c8y-cli/v2/pkg/completion" + "github.com/reubenmiller/go-c8y-cli/v2/pkg/flags" + "github.com/reubenmiller/go-c8y/pkg/c8y" + "github.com/spf13/cobra" + "gopkg.in/yaml.v3" +) + +var ( + PresetQueryInventory = "query-inventory" + PresetGetIdentity = "get-identity" + PresetQueryInventoryChildren = "query-inventory-children" +) + +type Command struct { + Name string `yaml:"name"` +} + +func ParseCommand(r io.Reader, factory *cmdutil.Factory, rootCmd *cobra.Command) (*cobra.Command, error) { + + log, err := factory.Logger() + if err != nil { + return nil, err + } + + spec := &models.Specification{} + b, err := io.ReadAll(r) + if err != nil { + return nil, err + } + err = yaml.Unmarshal(b, spec) + if err != nil { + return nil, err + } + + if spec.Group.Skip { + return nil, nil + } + + cmd := &cobra.Command{ + Use: spec.Group.Name, + Short: spec.Group.Description, + Long: spec.Group.DescriptionLong, + } + + // commands + for _, item := range spec.Commands { + if item.ShouldIgnore() { + continue + } + + log.Debugf("Adding command. name=%s", item.Name) + subcmd := NewCommandWithOptions(&cobra.Command{ + Use: item.Name, + Short: item.Description, + Long: item.GetDescriptionLong(), + Example: item.GetExamples(), + Hidden: item.IsHidden(), + }, item) + + // Use a preset to populate some common flags/options + if item.HasPreset() { + AddPredefinedGroupsFlags(subcmd, factory, item.Preset) + } + + flagNames := make(map[string]interface{}) + for _, param := range item.GetAllParameters() { + // Ignore duplicated flags + if _, ok := flagNames[param.Name]; ok { + continue + } + flagNames[param.Name] = 0 + + if err := AddFlag(subcmd, ¶m, factory); err != nil { + if file, ok := r.(*os.File); ok { + log.Warnf("Extension: Ignoring invalid flag. details=%s, command=%s, file=%s", err, item.Name, file.Name()) + } else { + log.Warnf("Extension: Ignoring invalid flag. details=%s, command=%s", err, item.Name) + } + // TODO: Is it better to be more forgiving or should it fail hard? + // return nil, err + continue + } + + if param.AcceptsPipeline() { + subcmd.Runtime = append(subcmd.Runtime, flags.WithExtendedPipelineSupport(param.Name, param.GetTargetProperty(), param.IsRequired(), param.PipelineAliases...)) + subcmd.Runtime = append(subcmd.Runtime, flags.WithPipelineAliases(param.Name, param.PipelineAliases...)) + } + + if len(param.ValidationSet) > 0 { + subcmd.Completion = append(subcmd.Completion, completion.WithValidateSet(param.Name, param.ValidationSet...)) + } + + // Add completions + subcmd.Completion = AppendCompletionOptions(subcmd.Completion, subcmd, ¶m, factory) + } + + // Misc. options + if item.CollectionProperty != "" { + subcmd.Runtime = append(subcmd.Runtime, flags.WithCollectionProperty(item.CollectionProperty)) + } + + if item.SemanticMethod != "" { + subcmd.Runtime = append(subcmd.Runtime, flags.WithSemanticMethod(item.SemanticMethod)) + } + + if item.IsDeprecated() { + subcmd.Runtime = append(subcmd.Runtime, flags.WithDeprecationNotice(item.Deprecated)) + } + + if item.SupportsProcessingMode() { + subcmd.Runtime = append(subcmd.Runtime, flags.WithProcessingMode()) + } + + // Add template/data support by default + if item.SupportsTemplates() { + subcmd.Runtime = append( + subcmd.Runtime, + flags.WithData(), + factory.WithTemplateFlag(subcmd.Command), + ) + } + + cmd.AddCommand(subcmd.NewRuntimeCommand(factory).SubCommand.GetCommand()) + } + + return cmd, nil +} + +func MapCommandAPI(cmd *CmdOptions, param *models.Parameter, typeName string) { + switch typeName { + case "string": + StringArg(cmd, param) + case "integer": + if v, err := strconv.ParseInt(param.Default, 10, 64); err == nil { + cmd.Command.Flags().Int64(param.Name, v, param.Description) + } + cmd.Command.Flags().Int64(param.Name, 0, param.Description) + } +} + +func AppendCompletionOptions(opts []completion.Option, cmd *CmdOptions, p *models.Parameter, factory *cmdutil.Factory) []completion.Option { + if len(p.ValidationSet) > 0 { + opts = append(opts, completion.WithValidateSet(p.Name, p.ValidationSet...)) + } + + if comp := GetCompletionOptions(cmd, p, factory); comp != nil { + opts = append(opts, comp) + } + return opts +} + +func GetCompletionOptions(cmd *CmdOptions, p *models.Parameter, factory *cmdutil.Factory) completion.Option { + // External flags + switch p.Completion.Type { + case "external": + return completion.WithExternalCompletion(p.Name, p.Completion.Command) + } + + // Internal flags + switch p.Type { + case "application", "applicationname": + return completion.WithApplication(p.Name, func() (*c8y.Client, error) { return factory.Client() }) + case "hostedapplication": + return completion.WithHostedApplication(p.Name, func() (*c8y.Client, error) { return factory.Client() }) + case "microservice": + return completion.WithMicroservice(p.Name, func() (*c8y.Client, error) { return factory.Client() }) + case "microservicename": + return completion.WithMicroservice(p.Name, func() (*c8y.Client, error) { return factory.Client() }) + case "microserviceinstance": + return completion.WithMicroserviceInstance(p.Name, p.GetDependentProperty(0, "id"), func() (*c8y.Client, error) { return factory.Client() }) + case "role[]", "roleself[]": + return completion.WithUserRole(p.Name, func() (*c8y.Client, error) { return factory.Client() }) + case "devicerequest", "devicerequest[]": + return completion.WithDeviceRegistrationRequest(p.Name, func() (*c8y.Client, error) { return factory.Client() }) + case "user[]", "userself[]": + return completion.WithUser(p.Name, func() (*c8y.Client, error) { return factory.Client() }) + case "usergroup[]": + return completion.WithUserGroup(p.Name, func() (*c8y.Client, error) { return factory.Client() }) + case "devicegroup[]": + return completion.WithDeviceGroup(p.Name, func() (*c8y.Client, error) { return factory.Client() }) + case "smartgroup[]": + return completion.WithSmartGroup(p.Name, func() (*c8y.Client, error) { return factory.Client() }) + case "tenant": + return completion.WithTenantID(p.Name, func() (*c8y.Client, error) { return factory.Client() }) + case "tenantname": + return completion.WithTenantID(p.Name, func() (*c8y.Client, error) { return factory.Client() }) + case "device[]": + return completion.WithDevice(p.Name, func() (*c8y.Client, error) { return factory.Client() }) + case "agent[]": + return completion.WithAgent(p.Name, func() (*c8y.Client, error) { return factory.Client() }) + case "software[]", "softwareName": + return completion.WithSoftware(p.Name, func() (*c8y.Client, error) { return factory.Client() }) + case "softwareversion[]", "softwareversionName", "softwareDetails": + return completion.WithSoftwareVersion(p.Name, p.GetDependentProperty(0, "software"), func() (*c8y.Client, error) { return factory.Client() }) + case "firmware[]": + return completion.WithFirmware(p.Name, func() (*c8y.Client, error) { return factory.Client() }) + case "firmwareversion[]", "firmwareversionName", "firmwareDetails": + return completion.WithFirmwareVersion(p.Name, p.GetDependentProperty(0, "firmware"), func() (*c8y.Client, error) { return factory.Client() }) + case "firmwarepatch[]", "firmwarepatchName": + return completion.WithFirmwarePatch(p.Name, p.GetDependentProperty(0, "firmware"), func() (*c8y.Client, error) { return factory.Client() }) + case "configuration[]", "configurationDetails": + return completion.WithConfiguration(p.Name, func() (*c8y.Client, error) { return factory.Client() }) + case "deviceprofile[]": + return completion.WithDeviceProfile(p.Name, func() (*c8y.Client, error) { return factory.Client() }) + case "deviceservice[]": + return completion.WithDeviceService(p.Name, p.GetDependentProperty(0, "device"), func() (*c8y.Client, error) { return factory.Client() }) + case "certificate[]": + return completion.WithDeviceCertificate(p.Name, func() (*c8y.Client, error) { return factory.Client() }) + case "subscriptionName": + return completion.WithNotification2SubscriptionName(p.Name, func() (*c8y.Client, error) { return factory.Client() }) + case "subscriptionId": + return completion.WithNotification2SubscriptionId(p.Name, func() (*c8y.Client, error) { return factory.Client() }) + } + return nil +} + +func AddFlag(cmd *CmdOptions, p *models.Parameter, factory *cmdutil.Factory) error { + log, err := factory.Logger() + if err != nil { + return err + } + existingFlag := cmd.Command.Flags().Lookup(p.Name) + if existingFlag != nil { + // TODO: Update the existing flag rather than ignoring it + // TODO: Should an error be returned? + log.Debugf("Ignoring duplicated flag. name=%s", p.Name) + return nil + } + switch p.Type { + case "string", "stringStatic", "devicerequest", "json_custom", "directory", "softwareName", "softwareversionName", "softwareDetails", "firmwareName", "firmwareversionName", "firmwarepatchName", "firmwareDetails", "binaryUploadURL", "inventoryChildType", "subscriptionName", "subscriptionId", "file", "attachment", "fileContents", "fileContentsAsString", "certificatefile": + cmd.Command.Flags().StringP(p.Name, p.ShortName, p.Default, p.Description) + + case "json": + // Ignore, as it is add by default to all PUT and POST requests + + case "datefrom", "dateto", "datetime", "date": + cmd.Command.Flags().StringP(p.Name, p.ShortName, p.Default, p.GetDescription()) + p.PipelineAliases = append(p.PipelineAliases, "time", "creationTime", "creationTime", "lastUpdated") + + case "source": + cmd.Command.Flags().StringP(p.Name, p.ShortName, p.Default, p.GetDescription()) + p.PipelineAliases = append(p.PipelineAliases, "id", "source.id", "managedObject.id", "deviceId") + + case "string[]", "stringcsv[]", "devicerequest[]", "software[]", "softwareversion[]", "firmware[]", "firmwareversion[]", "firmwarepatch[]", "configuration[]", "configurationDetails", "deviceprofile[]", "deviceservice[]", "id[]", "user[]", "userself[]", "certificate[]": + cmd.Command.Flags().StringSliceP(p.Name, p.ShortName, []string{p.Default}, p.GetDescription()) + + case "device[]", "agent[]": + cmd.Command.Flags().StringSliceP(p.Name, p.ShortName, []string{p.Default}, p.GetDescription()) + p.PipelineAliases = append(p.PipelineAliases, "deviceId", "source.id", "managedObject.id", "id") + + case "devicegroup[]": + cmd.Command.Flags().StringSliceP(p.Name, p.ShortName, []string{p.Default}, p.GetDescription()) + p.PipelineAliases = append(p.PipelineAliases, "source.id", "managedObject.id", "id") + + case "smartgroup[]": + cmd.Command.Flags().StringSliceP(p.Name, p.ShortName, []string{p.Default}, p.GetDescription()) + p.PipelineAliases = append(p.PipelineAliases, "managedObject.id") + + case "roleself[]": + cmd.Command.Flags().StringSliceP(p.Name, p.ShortName, []string{p.Default}, p.GetDescription()) + p.PipelineAliases = append(p.PipelineAliases, "self", "id") + + case "role[]", "usergroup[]": + cmd.Command.Flags().StringSliceP(p.Name, p.ShortName, []string{p.Default}, p.GetDescription()) + p.PipelineAliases = append(p.PipelineAliases, "id") + + case "application", "applicationname", "hostedapplication", "microservice", "microserviceinstance": + cmd.Command.Flags().StringP(p.Name, p.ShortName, p.Default, p.GetDescription()) + p.PipelineAliases = append(p.PipelineAliases, "id") + + case "microservicename": + cmd.Command.Flags().StringP(p.Name, p.ShortName, p.Default, p.GetDescription()) + p.PipelineAliases = append(p.PipelineAliases, "name") + + case "tenant", "tenantname": + cmd.Command.Flags().StringP(p.Name, p.ShortName, p.Default, p.GetDescription()) + p.PipelineAliases = append(p.PipelineAliases, "tenant", "owner.tenant.id") + + case "integer": + defaultValue, err := strconv.ParseInt(p.Default, 0, 64) + if err != nil { + defaultValue = 0 + } + cmd.Command.Flags().IntP(p.Name, p.ShortName, int(defaultValue), p.GetDescription()) + + case "float": + defaultValue, err := strconv.ParseFloat(p.Default, 32) + if err != nil { + defaultValue = 0 + } + cmd.Command.Flags().Float32P(p.Name, p.ShortName, float32(defaultValue), p.GetDescription()) + + case "boolean", "booleanDefault", "optional_fragment": + defaultValue, err := strconv.ParseBool(p.Default) + if err != nil { + defaultValue = false + } + cmd.Command.Flags().BoolP(p.Name, p.ShortName, defaultValue, p.GetDescription()) + + default: + return fmt.Errorf("unknown flag type. name=%s, type=%s", p.Name, p.Type) + } + + if p.IsRequired() && !p.AcceptsPipeline() { + cmd.Command.MarkFlagRequired(p.Name) + } + + if p.IsHidden() && !p.AcceptsPipeline() { + cmd.Command.Flags().MarkHidden(p.Name) + } + + return nil +} + +func GetOption(cmd *CmdOptions, p *models.Parameter, factory *cmdutil.Factory, args []string) []flags.GetOption { + targetProp := p.GetTargetProperty() + + opts := []flags.GetOption{} + + switch p.NamedLookup.Type { + case "external": + opts = append(opts, c8yfetcher.WithExternalCommandByNameFirstMatch(factory, args, p.NamedLookup.Command, p.NamedLookup.Options.IDPattern, p.Name, targetProp, p.Format)) + return opts + } + + // return early if options have already been set + if len(opts) > 0 { + return opts + } + + switch p.Type { + case "file": + opts = append(opts, flags.WithFormDataFileAndInfoWithTemplateSupport(cmdutil.NewTemplateResolver(factory), p.Name, flags.FlagDataName)...) + case "attachment": + opts = append(opts, flags.WithFormDataFile(p.Name, flags.FlagDataName)...) + + case "fileContents": + opts = append(opts, flags.WithFilePath(p.Name, targetProp, p.Value)) + case "fileContentsAsString": + opts = append(opts, flags.WithFileContentsAsString(p.Name, targetProp, p.Value)) + case "boolean": + opts = append(opts, flags.WithBoolValue(p.Name, targetProp, p.Value)) + case "booleanDefault": + opts = append(opts, flags.WithDefaultBoolValue(p.Name, targetProp, p.Value)) + case "optional_fragment": + opts = append(opts, flags.WithOptionalFragment(p.Name, targetProp, p.Value)) + + case "datetime": + if p.TargetType == models.ParamPath || p.TargetType == models.ParamQueryParameter { + opts = append(opts, flags.WithEncodedRelativeTimestamp(p.Name, targetProp, p.Format)) + } else { + opts = append(opts, flags.WithRelativeTimestamp(p.Name, targetProp, p.Format)) + } + case "date": + opts = append(opts, flags.WithRelativeDate(false, p.Name, targetProp, p.Format)) + + case "string[]": + opts = append(opts, flags.WithStringSliceValues(p.Name, targetProp, p.Value)) + case "stringcsv[]": + opts = append(opts, flags.WithStringSliceCSV(p.Name, targetProp, p.Value)) + + case "inventoryChildType": + opts = append(opts, flags.WithInventoryChildType(p.Name, targetProp, p.Format)) + + case "string", "source", "tenantname", "devicerequest", "subscriptionName", "subscriptionId", "applicationname", "microserviceinstance", "microservicename", "softwareName", "softwareversionName", "firmwareName", "firmwareversionName", "firmwarepatchName": + opts = append(opts, flags.WithStringValue(p.Name, targetProp, p.Format)) + + case "stringStatic": + opts = append(opts, flags.WithStaticStringValue(p.Name, p.Value)) + case "integer": + opts = append(opts, flags.WithIntValue(p.Name, targetProp, p.Format)) + case "float": + opts = append(opts, flags.WithFloatValue(p.Name, targetProp, p.Format)) + + case "json_custom": + opts = append(opts, flags.WithDataValue(p.Name, targetProp, p.Format)) + case "binaryUploadURL": + opts = append(opts, c8ybinary.WithBinaryUploadURL(factory.Client, factory.IOStreams.ProgressIndicator(), p.Name, targetProp, p.Format)) + case "json": + // don't do anything because it should be manually set) + + case "tenant": + opts = append(opts, flags.WithStringDefaultValue(factory.GetTenant(), p.Name, targetProp, p.Format)) + + case "id[]", "devicerequest[]": + opts = append(opts, c8yfetcher.WithIDSlice(args, p.Name, targetProp, p.Format)) + + case "application": + opts = append(opts, c8yfetcher.WithApplicationByNameFirstMatch(factory, args, p.Name, targetProp, p.Format)) + + case "hostedapplication": + opts = append(opts, c8yfetcher.WithHostedApplicationByNameFirstMatch(factory, args, p.Name, targetProp, p.Format)) + + case "microservice": + opts = append(opts, c8yfetcher.WithMicroserviceByNameFirstMatch(factory, args, p.Name, targetProp, p.Format)) + + case "software[]": + opts = append(opts, c8yfetcher.WithSoftwareByNameFirstMatch(factory, args, p.Name, targetProp, p.Format)) + + case "softwareDetails": + opts = append(opts, c8yfetcher.WithSoftwareVersionData(factory, p.GetDependentProperty(0, "software"), p.Name, p.GetDependentProperty(1, "url"), args, "", targetProp, p.Format)) + + case "configurationDetails": + opts = append(opts, c8yfetcher.WithConfigurationFileData(factory, p.Name, p.GetDependentProperty(0, "configurationType"), p.GetDependentProperty(1, "url"), args, "", targetProp, p.Format)) + + case "softwareversion[]": + opts = append(opts, c8yfetcher.WithSoftwareVersionByNameFirstMatch(factory, p.GetDependentProperty(0, "software"), args, p.Name, targetProp, p.Format)) + + case "deviceservice[]": + opts = append(opts, c8yfetcher.WithDeviceServiceByNameFirstMatch(factory, p.GetDependentProperty(0, "device"), args, p.Name, targetProp, p.Format)) + + case "certificatefile": + opts = append(opts, flags.WithCertificateFile(p.Name, targetProp)) + case "certificate[]": + opts = append(opts, c8yfetcher.WithCertificateByNameFirstMatch(factory, args, p.Name, targetProp)) + + case "firmware[]": + opts = append(opts, c8yfetcher.WithFirmwareByNameFirstMatch(factory, args, p.Name, targetProp, p.Format)) + case "firmwareversion[]": + opts = append(opts, c8yfetcher.WithFirmwareVersionByNameFirstMatch(factory, p.GetDependentProperty(0, "firmware"), args, p.Name, targetProp, p.Format)) + case "firmwareDetails": + opts = append(opts, c8yfetcher.WithFirmwareVersionData(factory, p.GetDependentProperty(0, "firmware"), p.Name, p.GetDependentProperty(1, "url"), args, "", targetProp)) + case "firmwarepatch[]": + opts = append(opts, c8yfetcher.WithFirmwarePatchByNameFirstMatch(factory, p.GetDependentProperty(0, "firmware"), args, p.Name, targetProp)) + + case "configuration[]": + opts = append(opts, c8yfetcher.WithConfigurationByNameFirstMatch(factory, args, p.Name, targetProp)) + + case "deviceprofile[]": + opts = append(opts, c8yfetcher.WithDeviceProfileByNameFirstMatch(factory, args, p.Name, targetProp)) + + case "device[]": + opts = append(opts, c8yfetcher.WithDeviceByNameFirstMatch(factory, args, p.Name, targetProp, p.Format)) + + case "agent[]": + opts = append(opts, c8yfetcher.WithAgentByNameFirstMatch(factory, args, p.Name, targetProp, p.Format)) + + case "devicegroup[]": + opts = append(opts, c8yfetcher.WithDeviceGroupByNameFirstMatch(factory, args, p.Name, targetProp, p.Format)) + + case "smartgroup[]": + opts = append(opts, c8yfetcher.WithSmartGroupByNameFirstMatch(factory, args, p.Name, targetProp, p.Format)) + + case "user[]": + opts = append(opts, c8yfetcher.WithUserByNameFirstMatch(factory, args, p.Name, targetProp, p.Format)) + + case "userself[]": + opts = append(opts, c8yfetcher.WithUserSelfByNameFirstMatch(factory, args, p.Name, targetProp, p.Format)) + + case "roleself[]": + opts = append(opts, c8yfetcher.WithRoleSelfByNameFirstMatch(factory, args, p.Name, targetProp, p.Format)) + + case "role[]": + opts = append(opts, c8yfetcher.WithRoleByNameFirstMatch(factory, args, p.Name, targetProp, p.Format)) + + case "usergroup[]": + opts = append(opts, c8yfetcher.WithUserGroupByNameFirstMatch(factory, args, p.Name, targetProp, p.Format)) + } + + return opts +} + +func AddPredefinedGroupsFlags(cmd *CmdOptions, factory *cmdutil.Factory, template models.CommandPreset) { + + queryOptions := []flags.GetOption{} + switch template.Type { + case PresetGetIdentity: + cmd.Spec.Method = "GET" + + if identityType := cmd.Spec.Preset.GetOption("value"); identityType != "" { + cmd.Spec.Path = fmt.Sprintf("/identity/externalIds/%s/{name}", identityType) + cmd.Command.Flags().String("name", "", "External identity id/name (required) (accepts pipeline)") + cmd.Path.Options = append( + cmd.Path.Options, + []flags.GetOption{ + flags.WithStringValue("name", "name", "%s"), + }..., + ) + } else { + cmd.Spec.Path = "/identity/externalIds/{type}/{name}" + cmd.Command.Flags().String("type", "c8y_Serial", "External identity type") + cmd.Command.Flags().String("name", "", "External identity id/name (required) (accepts pipeline)") + cmd.Path.Options = append( + cmd.Path.Options, + []flags.GetOption{ + flags.WithStringValue("type", "type", "%s"), + flags.WithStringValue("name", "name", "%s"), + }..., + ) + } + + case PresetQueryInventoryChildren: + cmd.Spec.Method = "GET" + cmd.Spec.Path = fmt.Sprintf("inventory/managedObjects/{id}/%s", cmd.Spec.Preset.GetOption("type", "childDevices")) + + cmd.Command.Flags().StringSlice("id", []string{""}, "Managed object id. (required) (accepts pipeline)") + cmd.Command.Flags().String("query", "", "Additional query filter") + cmd.Command.Flags().String("queryTemplate", "", "String template to be used when applying the given query. Use %s to reference the query/pipeline input") + cmd.Command.Flags().String("orderBy", "name", "Order by. e.g. _id asc or name asc or creationTime.date desc") + cmd.Command.Flags().String("name", "", "Filter by name") + cmd.Command.Flags().String("type", "", "Filter by type") + cmd.Command.Flags().Bool("agents", false, "Only include agents") + cmd.Command.Flags().String("fragmentType", "", "Filter by fragment type") + cmd.Command.Flags().String("owner", "", "Filter by owner") + cmd.Command.Flags().String("availability", "", "Filter by c8y_Availability.status") + cmd.Command.Flags().String("lastMessageDateTo", "", "Filter c8y_Availability.lastMessage to a specific date") + cmd.Command.Flags().String("lastMessageDateFrom", "", "Filter c8y_Availability.lastMessage from a specific date") + cmd.Command.Flags().String("creationTimeDateTo", "", "Filter creationTime.date to a specific date") + cmd.Command.Flags().String("creationTimeDateFrom", "", "Filter creationTime.date from a specific date") + // cmd.Command.Flags().StringSlice("group", []string{""}, "Filter by group inclusion") + cmd.Command.Flags().Bool("skipChildrenNames", false, "Don't include the child devices names in the response. This can improve the API response because the names don't need to be retrieved") + cmd.Command.Flags().Bool("withChildren", false, "Determines if children with ID and name should be returned when fetching the managed object. Set it to false to improve query performance.") + cmd.Command.Flags().Bool("withChildrenCount", false, "When set to true, the returned result will contain the total number of children in the respective objects (childAdditions, childAssets and childDevices)") + cmd.Command.Flags().Bool("withGroups", false, "When set to true it returns additional information about the groups to which the searched managed object belongs. This results in setting the assetParents property with additional information about the groups.") + cmd.Command.Flags().Bool("withParents", false, "Include a flat list of all parents and grandparents of the given object") + + cmd.Path.Options = append( + cmd.Path.Options, + []flags.GetOption{ + // c8yfetcher.WithDeviceByNameFirstMatch(client, args, "id", "id"), + flags.WithStringSliceValues("id", "id", "%s"), + }..., + ) + + c8yQueryOptions := []flags.GetOption{ + flags.WithStaticStringValue("fixed", template.GetOption("value")), + flags.WithStringValue("query", "query", "%s"), + flags.WithStringValue("name", "name", "(name eq '%s')"), + flags.WithStringValue("type", "type", "(type eq '%s')"), + flags.WithDefaultBoolValue("agents", "agents", "has(com_cumulocity_model_Agent)"), + flags.WithStringValue("fragmentType", "fragmentType", "has(%s)"), + flags.WithStringValue("owner", "owner", "(owner eq '%s')"), + flags.WithStringValue("availability", "availability", "(c8y_Availability.status eq '%s')"), + flags.WithEncodedRelativeTimestamp("lastMessageDateTo", "lastMessageDateTo", "(c8y_Availability.lastMessage le '%s')"), + flags.WithEncodedRelativeTimestamp("lastMessageDateFrom", "lastMessageDateFrom", "(c8y_Availability.lastMessage ge '%s')"), + flags.WithEncodedRelativeTimestamp("creationTimeDateTo", "creationTimeDateTo", "(creationTime.date le '%s')"), + flags.WithEncodedRelativeTimestamp("creationTimeDateFrom", "creationTimeDateFrom", "(creationTime.date ge '%s')"), + // c8yfetcher.WithDeviceGroupByNameFirstMatch(client, args, "group", "group", "bygroupid(%s)"), + } + + // Add extensions to cumulocity query builder + for _, p := range template.Extensions { + c8yQueryOptions = append(c8yQueryOptions, GetOption(cmd, &p, factory, nil)...) + } + + // options + queryOptions = append( + queryOptions, + + flags.WithBoolValue("skipChildrenNames", "skipChildrenNames", ""), + flags.WithBoolValue("withChildren", "withChildren", ""), + flags.WithBoolValue("withChildrenCount", "withChildrenCount", ""), + flags.WithBoolValue("withGroups", "withGroups", ""), + flags.WithBoolValue("withParents", "withParents", ""), + + flags.WithCumulocityQuery( + c8yQueryOptions, + template.GetOption("param", "query"), + ), + ) + + cmd.Runtime = append(cmd.Runtime, + flags.WithExtendedPipelineSupport("id", "id", true, "deviceId", "source.id", "managedObject.id", "id"), + flags.WithPipelineAliases("id", "deviceId", "source.id", "managedObject.id", "id"), + ) + + case PresetQueryInventory: + // Cumulocity inventory query + cmd.Spec.Method = "GET" + cmd.Spec.Path = "inventory/managedObjects" + + // flags + cmd.Command.Flags().String("query", "", "Additional query filter (accepts pipeline)") + cmd.Command.Flags().String("queryTemplate", "", "String template to be used when applying the given query. Use %s to reference the query/pipeline input") + cmd.Command.Flags().String("orderBy", "name", "Order by. e.g. _id asc or name asc or creationTime.date desc") + cmd.Command.Flags().String("name", "", "Filter by name") + cmd.Command.Flags().String("type", "", "Filter by type") + cmd.Command.Flags().Bool("agents", false, "Only include agents") + cmd.Command.Flags().String("fragmentType", "", "Filter by fragment type") + cmd.Command.Flags().String("owner", "", "Filter by owner") + cmd.Command.Flags().String("availability", "", "Filter by c8y_Availability.status") + cmd.Command.Flags().String("lastMessageDateTo", "", "Filter c8y_Availability.lastMessage to a specific date") + cmd.Command.Flags().String("lastMessageDateFrom", "", "Filter c8y_Availability.lastMessage from a specific date") + cmd.Command.Flags().String("creationTimeDateTo", "", "Filter creationTime.date to a specific date") + cmd.Command.Flags().String("creationTimeDateFrom", "", "Filter creationTime.date from a specific date") + cmd.Command.Flags().StringSlice("group", []string{""}, "Filter by group inclusion") + cmd.Command.Flags().Bool("skipChildrenNames", false, "Don't include the child devices names in the response. This can improve the API response because the names don't need to be retrieved") + cmd.Command.Flags().Bool("withChildren", false, "Determines if children with ID and name should be returned when fetching the managed object. Set it to false to improve query performance.") + cmd.Command.Flags().Bool("withChildrenCount", false, "When set to true, the returned result will contain the total number of children in the respective objects (childAdditions, childAssets and childDevices)") + cmd.Command.Flags().Bool("withGroups", false, "When set to true it returns additional information about the groups to which the searched managed object belongs. This results in setting the assetParents property with additional information about the groups.") + cmd.Command.Flags().Bool("withParents", false, "Include a flat list of all parents and grandparents of the given object") + + // completions + cmd.Completion = append( + cmd.Completion, + completion.WithValidateSet("availability", "AVAILABLE", "UNAVAILABLE", "MAINTENANCE"), + completion.WithDeviceGroup("group", func() (*c8y.Client, error) { return factory.Client() }), + ) + + c8yQueryOptions := []flags.GetOption{ + flags.WithStaticStringValue("fixed", template.GetOption("value")), + flags.WithStringValue("query", "query", "%s"), + flags.WithStringValue("name", "name", "(name eq '%s')"), + flags.WithStringValue("type", "type", "(type eq '%s')"), + flags.WithDefaultBoolValue("agents", "agents", "has(com_cumulocity_model_Agent)"), + flags.WithStringValue("fragmentType", "fragmentType", "has(%s)"), + flags.WithStringValue("owner", "owner", "(owner eq '%s')"), + flags.WithStringValue("availability", "availability", "(c8y_Availability.status eq '%s')"), + flags.WithEncodedRelativeTimestamp("lastMessageDateTo", "lastMessageDateTo", "(c8y_Availability.lastMessage le '%s')"), + flags.WithEncodedRelativeTimestamp("lastMessageDateFrom", "lastMessageDateFrom", "(c8y_Availability.lastMessage ge '%s')"), + flags.WithEncodedRelativeTimestamp("creationTimeDateTo", "creationTimeDateTo", "(creationTime.date le '%s')"), + flags.WithEncodedRelativeTimestamp("creationTimeDateFrom", "creationTimeDateFrom", "(creationTime.date ge '%s')"), + c8yfetcher.WithDeviceGroupByNameFirstMatch(factory, []string{}, "group", "group", "bygroupid(%s)"), + } + + // Add extensions to cumulocity query builder + for _, p := range template.Extensions { + c8yQueryOptions = append(c8yQueryOptions, GetOption(cmd, &p, factory, nil)...) + } + + // options + queryOptions = append( + queryOptions, + + flags.WithBoolValue("skipChildrenNames", "skipChildrenNames", ""), + flags.WithBoolValue("withChildren", "withChildren", ""), + flags.WithBoolValue("withChildrenCount", "withChildrenCount", ""), + flags.WithBoolValue("withGroups", "withGroups", ""), + flags.WithBoolValue("withParents", "withParents", ""), + + flags.WithCumulocityQuery( + c8yQueryOptions, + template.GetOption("param", "q"), + ), + ) + + cmd.Runtime = append(cmd.Runtime, + flags.WithExtendedPipelineSupport("query", "query", false, "c8y_DeviceQueryString"), + flags.WithPipelineAliases("lastMessageDateTo", "time", "creationTime", "lastUpdated"), + flags.WithPipelineAliases("lastMessageDateFrom", "time", "creationTime", "lastUpdated"), + flags.WithPipelineAliases("creationTimeDateTo", "time", "creationTime", "lastUpdated"), + flags.WithPipelineAliases("creationTimeDateFrom", "time", "creationTime", "lastUpdated"), + flags.WithPipelineAliases("group", "source.id", "managedObject.id", "id"), + + flags.WithCollectionProperty("managedObjects"), + ) + } + + // Add flags/completions for preset extensions + for _, p := range template.Extensions { + AddFlag(cmd, &p, factory) + cmd.Completion = AppendCompletionOptions(cmd.Completion, cmd, &p, factory) + } + + cmd.QueryParameter = append(cmd.QueryParameter, queryOptions...) +} + +var ( + CommonFlagsQuery = "query" + CommonFlagsTemplate = "queryTemplate" + CommonFlagsOrderBy = "orderBy" + CommonFlagsName = "name" + CommonFlagsType = "type" + CommonFlagsAgents = "agents" + CommonFlagsFragmentType = "fragmentType" + CommonFlagsOwner = "owner" + CommonFlagsAvailability = "availability" + CommonFlagsLastMessageDateTo = "lastMessageDateTo" + CommonFlagsLastMessageDateFrom = "lastMessageDateFrom" + CommonFlagsCreationTimeDateTo = "creationTimeDateTo" + CommonFlagsCreationTimeDateFrom = "creationTimeDateFrom" + CommonFlagsGroup = "group" + CommonFlagsSkipChildrenNames = "skipChildrenNames" + CommonFlagsWithChildren = "withChildren" + CommonFlagsWithChildrenCount = "withChildrenCount" + CommonFlagsWithGroups = "withGroups" + CommonFlagsWithParents = "withParents" +) + +type CmdOptions struct { + Spec models.Command + Completion []completion.Option + Command *cobra.Command + Runtime []flags.Option + Header []flags.GetOption + QueryParameter []flags.GetOption + FormData []flags.GetOption + Body BodyOptions + Path PathOptions +} + +type BodyOptions struct { + Options []flags.GetOption + IsBinary bool + Initialize bool + UploadProgressSource string +} + +func (c *CmdOptions) NewRuntimeCommand(f *cmdutil.Factory) *RuntimeCmd { + return NewRuntimeCmd(f, c) +} + +func NewCommandWithOptions(cmd *cobra.Command, endpoint models.Command) *CmdOptions { + return &CmdOptions{ + Spec: endpoint, + Command: cmd, + Runtime: make([]flags.Option, 0), + Completion: make([]completion.Option, 0), + Header: make([]flags.GetOption, 0), + QueryParameter: make([]flags.GetOption, 0), + FormData: make([]flags.GetOption, 0), + Body: BodyOptions{ + Options: make([]flags.GetOption, 0), + }, + Path: PathOptions{ + Options: make([]flags.GetOption, 0), + }, + } +} + +type PathOptions struct { + Template string + Options []flags.GetOption +} + +func StringArg(cmd *CmdOptions, param *models.Parameter) { + cmd.Command.Flags().String(param.Name, param.Default, param.Description) +} diff --git a/pkg/cmdparser/runtime_command.go b/pkg/cmdparser/runtime_command.go new file mode 100644 index 000000000..2765241d9 --- /dev/null +++ b/pkg/cmdparser/runtime_command.go @@ -0,0 +1,337 @@ +package cmdparser + +import ( + "fmt" + "io" + "net/http" + + "github.com/reubenmiller/go-c8y-cli/v2/pkg/c8ybinary" + "github.com/reubenmiller/go-c8y-cli/v2/pkg/cmd/subcommand" + "github.com/reubenmiller/go-c8y-cli/v2/pkg/cmderrors" + "github.com/reubenmiller/go-c8y-cli/v2/pkg/cmdutil" + "github.com/reubenmiller/go-c8y-cli/v2/pkg/completion" + "github.com/reubenmiller/go-c8y-cli/v2/pkg/flags" + "github.com/reubenmiller/go-c8y-cli/v2/pkg/mapbuilder" + "github.com/reubenmiller/go-c8y/pkg/c8y" + "github.com/spf13/cobra" +) + +type RuntimeCmd struct { + *subcommand.SubCommand + + factory *cmdutil.Factory + options *CmdOptions +} + +// NewRuntimeCmd creates a command which is created at runtime +func NewRuntimeCmd(f *cmdutil.Factory, options *CmdOptions) *RuntimeCmd { + ccmd := &RuntimeCmd{ + factory: f, + options: options, + } + cmd := options.Command + cmd.PreRunE = func(cmd *cobra.Command, args []string) error { + // Mode checks + switch options.Spec.GetMethod() { + case "POST": + return f.CreateModeEnabled() + case "PUT": + return f.UpdateModeEnabled() + case "DELETE": + return f.DeleteModeEnabled() + } + return nil + } + cmd.RunE = ccmd.RunE + cmd.SilenceUsage = true + + completion.WithOptions( + cmd, + options.Completion..., + ) + + flags.WithOptions( + cmd, + options.Runtime..., + ) + + ccmd.SubCommand = subcommand.NewSubCommand(cmd) + return ccmd +} + +func (n *RuntimeCmd) Prepare(args []string) error { + item := n.options.Spec + subcmd := n.options + factory := n.factory + + cfg, err := factory.Config() + if err != nil { + return err + } + + // Presets + if subcmd.Spec.HasPreset() { + var values *[]flags.GetOption + switch subcmd.Spec.Preset.Type { + case PresetGetIdentity: + values = &subcmd.QueryParameter + } + if values != nil { + for _, p := range subcmd.Spec.Preset.Extensions { + *values = append(*values, GetOption(subcmd, &p, factory, args)...) + } + } + } + + // path + for _, p := range item.PathParameters { + subcmd.Path.Options = append(subcmd.Path.Options, GetOption(subcmd, &p, factory, args)...) + } + subcmd.Path.Template = item.Path + + // header + subcmd.Header = append(subcmd.Header, flags.WithCustomStringSlice(func() ([]string, error) { return cfg.GetHeader(), nil }, "header")) + for _, p := range item.HeaderParameters { + subcmd.Header = append(subcmd.Header, GetOption(subcmd, &p, factory, args)...) + } + + if subcmd.Spec.ContentType != "" { + subcmd.Header = append(subcmd.Header, flags.WithStaticStringValue("Content-Type", subcmd.Spec.ContentType)) + } + + if subcmd.Spec.Accept != "" { + subcmd.Header = append(subcmd.Header, flags.WithStaticStringValue("Accept", subcmd.Spec.Accept)) + } + + if item.SupportsProcessingMode() { + subcmd.Header = append(subcmd.Header, flags.WithProcessingModeValue()) + } + + // query + subcmd.QueryParameter = append(subcmd.QueryParameter, flags.WithCustomStringSlice(func() ([]string, error) { return cfg.GetQueryParameters(), nil }, "custom")) + + for _, p := range item.QueryParameters { + subcmd.QueryParameter = append(subcmd.QueryParameter, GetOption(subcmd, &p, factory, args)...) + + // Support Cumulocity Query builder + if len(p.Children) > 0 { + queryOptions := []flags.GetOption{} + for _, child := range p.Children { + // Ignore special in-built values as these are handled separately + if child.Name == "queryTemplate" || child.Name == "orderBy" { + continue + } + queryOptions = append(queryOptions, GetOption(subcmd, &child, factory, args)...) + } + + if subcmd.Spec.HasPreset() { + switch subcmd.Spec.Preset.Type { + case PresetQueryInventory: + for _, p := range subcmd.Spec.Preset.Extensions { + queryOptions = append(queryOptions, GetOption(subcmd, &p, factory, args)...) + } + case PresetQueryInventoryChildren: + for _, p := range subcmd.Spec.Preset.Extensions { + queryOptions = append(queryOptions, GetOption(subcmd, &p, factory, args)...) + } + } + } + + subcmd.QueryParameter = append(subcmd.QueryParameter, flags.WithCumulocityQuery(queryOptions, p.GetTargetProperty())) + } + } + + // body + requiredBodyKeys := []string{} + requiredBodyKeys = append(requiredBodyKeys, item.BodyRequiredKeys...) + for _, p := range item.Body { + if p.IsRequired() { + requiredBodyKeys = append(requiredBodyKeys, p.GetTargetProperty()) + } + } + if len(requiredBodyKeys) > 0 { + subcmd.Body.Options = append(subcmd.Body.Options, flags.WithRequiredProperties(requiredBodyKeys...)) + } + + if len(item.Body) > 0 { + if item.Method == "PUT" || item.Method == "POST" { + subcmd.Body.Initialize = true + } + } + + supportsFormData := false + switch item.GetBodyContentType() { + case "binary": + subcmd.Body.IsBinary = true + supportsFormData = true + case "formdata": + supportsFormData = true + subcmd.Body.Options = append(subcmd.Body.Options, flags.WithDataFlagValue()) + default: + subcmd.Body.Options = append(subcmd.Body.Options, flags.WithDataFlagValue()) + } + for _, p := range item.Body { + switch p.Type { + case "file", "attachment": + subcmd.Body.UploadProgressSource = p.Name + subcmd.FormData = append(subcmd.FormData, GetOption(subcmd, &p, factory, args)...) + case "fileContents": + subcmd.Body.UploadProgressSource = p.Name + fallthrough + default: + if supportsFormData { + subcmd.FormData = append(subcmd.FormData, GetOption(subcmd, &p, factory, args)...) + } else { + subcmd.Body.Options = append(subcmd.Body.Options, GetOption(subcmd, &p, factory, args)...) + } + } + } + + subcmd.Body.Options = append(subcmd.Body.Options, cmdutil.WithTemplateValue(factory)) + subcmd.Body.Options = append(subcmd.Body.Options, flags.WithTemplateVariablesValue()) + + for _, bodyTemplate := range item.BodyTemplates { + if bodyTemplate.Type == "jsonnet" { + if bodyTemplate.ApplyLast { + subcmd.Body.Options = append(subcmd.Body.Options, flags.WithRequiredTemplateString(bodyTemplate.Template)) + } else { + subcmd.Body.Options = append(subcmd.Body.Options, flags.WithDefaultTemplateString(bodyTemplate.Template)) + } + } + } + + return nil +} + +// RunE executes the command +func (n *RuntimeCmd) RunE(cmd *cobra.Command, args []string) error { + + if err := n.Prepare(args); err != nil { + return err + } + + cfg, err := n.factory.Config() + if err != nil { + return err + } + // Runtime flag options + flags.WithOptions( + cmd, + flags.WithRuntimePipelineProperty(), + ) + client, err := n.factory.Client() + if err != nil { + return err + } + inputIterators, err := cmdutil.NewRequestInputIterators(cmd, cfg) + if err != nil { + return err + } + + // query parameters + query := flags.NewQueryTemplate() + err = flags.WithQueryParameters( + cmd, + query, + inputIterators, + n.options.QueryParameter..., + ) + if err != nil { + return cmderrors.NewUserError(err) + } + commonOptions, err := cfg.GetOutputCommonOptions(cmd) + if err != nil { + return cmderrors.NewUserError(fmt.Sprintf("Failed to get common options. err=%s", err)) + } + commonOptions.AddQueryParameters(query) + + queryValue, err := query.GetQueryUnescape(true) + + if err != nil { + return cmderrors.NewSystemError("Invalid query parameter") + } + + // headers + headers := http.Header{} + err = flags.WithHeaders( + cmd, + headers, + inputIterators, + n.options.Header..., + ) + if err != nil { + return cmderrors.NewUserError(err) + } + + // form data + formData := make(map[string]io.Reader) + err = flags.WithFormDataOptions( + cmd, + formData, + inputIterators, + n.options.FormData..., + ) + if err != nil { + return cmderrors.NewUserError(err) + } + + // body + body := mapbuilder.NewInitializedMapBuilder(n.options.Body.Initialize) + err = flags.WithBody( + cmd, + body, + inputIterators, + n.options.Body.Options..., + ) + if err != nil { + return cmderrors.NewUserError(err) + } + + // path parameters + path := flags.NewStringTemplate(n.options.Path.Template) + err = flags.WithPathParameters( + cmd, + path, + inputIterators, + n.options.Path.Options..., + ) + if err != nil { + return err + } + + var req *c8y.RequestOptions + if n.options.Body.IsBinary { + req = &c8y.RequestOptions{ + Method: n.options.Spec.Method, + Path: path.GetTemplate(), + Query: queryValue, + Body: body.GetFileContents(), + FormData: formData, + Header: headers, + IgnoreAccept: cfg.IgnoreAcceptHeader(), + DryRun: cfg.ShouldUseDryRun(cmd.CommandPath()), + } + } else { + req = &c8y.RequestOptions{ + Method: n.options.Spec.Method, + Path: path.GetTemplate(), + Query: queryValue, + Body: body, + FormData: formData, + Header: headers, + IgnoreAccept: cfg.IgnoreAcceptHeader(), + DryRun: cfg.ShouldUseDryRun(cmd.CommandPath()), + } + } + + // add upload progress bar + if n.options.Body.UploadProgressSource != "" { + req.PrepareRequest = c8ybinary.AddProgress( + cmd, + n.options.Body.UploadProgressSource, + cfg.GetProgressBar(n.factory.IOStreams.ErrOut, n.factory.IOStreams.IsStderrTTY()), + ) + } + + return n.factory.RunWithWorkers(client, cmd, req, inputIterators) +} diff --git a/pkg/cmdutil/factory.go b/pkg/cmdutil/factory.go index e70a7180b..002b7a57e 100644 --- a/pkg/cmdutil/factory.go +++ b/pkg/cmdutil/factory.go @@ -6,6 +6,7 @@ import ( "fmt" "io/ioutil" "log" + "path/filepath" "strings" "github.com/reubenmiller/go-c8y-cli/v2/pkg/activitylogger" @@ -14,11 +15,13 @@ import ( "github.com/reubenmiller/go-c8y-cli/v2/pkg/console" "github.com/reubenmiller/go-c8y-cli/v2/pkg/dataview" "github.com/reubenmiller/go-c8y-cli/v2/pkg/encrypt" + "github.com/reubenmiller/go-c8y-cli/v2/pkg/extensions" "github.com/reubenmiller/go-c8y-cli/v2/pkg/flags" "github.com/reubenmiller/go-c8y-cli/v2/pkg/iostreams" "github.com/reubenmiller/go-c8y-cli/v2/pkg/jsonformatter" "github.com/reubenmiller/go-c8y-cli/v2/pkg/logger" "github.com/reubenmiller/go-c8y-cli/v2/pkg/mode" + "github.com/reubenmiller/go-c8y-cli/v2/pkg/pathresolver" "github.com/reubenmiller/go-c8y-cli/v2/pkg/request" "github.com/reubenmiller/go-c8y-cli/v2/pkg/worker" "github.com/reubenmiller/go-c8y/pkg/c8y" @@ -43,6 +46,9 @@ type Factory struct { BuildVersion string BuildBranch string + // Extension + ExtensionManager func() extensions.ExtensionManager + // Executable is the path to the currently invoked binary Executable string } @@ -183,7 +189,7 @@ func (f *Factory) RunWithWorkers(client *c8y.Client, cmd *cobra.Command, req *c8 // GetViewProperties Look up the view properties to display func (f *Factory) GetViewProperties(cfg *config.Config, cmd *cobra.Command, output []byte) ([]string, error) { - dataview, err := f.DataView() + dataView, err := f.DataView() if err != nil { return nil, err } @@ -205,7 +211,9 @@ func (f *Factory) GetViewProperties(cfg *config.Config, cmd *cobra.Command, outp return []string{"**"}, nil case config.ViewsAuto: jsonResponse := gjson.ParseBytes(output) - props, err := dataview.GetView(&jsonResponse, "") + props, err := dataView.GetView(&dataview.ViewData{ + ResponseBody: &jsonResponse, + }) if err != nil || len(props) == 0 { if err != nil { @@ -220,7 +228,7 @@ func (f *Factory) GetViewProperties(cfg *config.Config, cmd *cobra.Command, outp } default: // manual view - props, err := dataview.GetViewByName(view) + props, err := dataView.GetViewByName(view) if err != nil || len(props) == 0 { if err != nil { cfg.Logger.Warnf("no matching view found. %s, name=%s", err, view) @@ -269,7 +277,7 @@ func (f *Factory) WriteJSONToConsole(cfg *config.Config, cmd *cobra.Command, pro false, jsonformatter.WithFileOutput(commonOptions.OutputFile != "", commonOptions.OutputFile, false), jsonformatter.WithTrimSpace(true), - jsonformatter.WithJSONStreamOutput(true, consol.IsJSONStream(), consol.IsCSV()), + jsonformatter.WithJSONStreamOutput(true, consol.IsJSONStream(), consol.IsTextOutput()), jsonformatter.WithSuffix(len(output) > 0, "\n"), ) return nil @@ -355,6 +363,69 @@ func (f *Factory) CheckPostCommandError(err error) error { return outErr } +func (f *Factory) ResolveTemplates(pattern string, withFullPath bool) ([]string, error) { + cfg, err := f.Config() + if err != nil { + return nil, err + } + paths := cfg.GetTemplatePaths() + + allMatches := []string{} + + // Filter + matches, err := pathresolver.ResolvePaths(paths, "*", []string{".jsonnet"}, "ignore") + if err != nil { + return []string{"jsonnet"}, err + } + + // Apply full matches + for _, m := range matches { + option := filepath.Base(m) + if matched, _ := filepath.Match(pattern, option); matched { + if withFullPath { + allMatches = append(allMatches, m) + } else { + allMatches = append(allMatches, option) + } + } + } + + // Extensions + for _, ext := range f.ExtensionManager().List() { + extTemplatePath := ext.TemplatePath() + if extTemplatePath == "" { + continue + } + matches, err := pathresolver.ResolvePaths([]string{extTemplatePath}, "*", []string{".jsonnet"}, "ignore") + if err != nil { + return []string{"jsonnet"}, err + } + + // Apply full matches + for _, m := range matches { + option := fmt.Sprintf("%s::%s", ext.Name(), filepath.Base(m)) + if matched, _ := filepath.Match(pattern, option); matched { + if withFullPath { + allMatches = append(allMatches, m) + } else { + allMatches = append(allMatches, option) + } + } + } + + } + + return allMatches, nil +} + +func (f *Factory) GetTenant() string { + client, err := f.Client() + if err != nil { + return "" + } + return client.TenantName +} + // NewRequestInputIterators create a request iterator based on pipe line configuration func NewRequestInputIterators(cmd *cobra.Command, cfg *config.Config) (*flags.RequestInputIterators, error) { pipeOpts, err := flags.GetPipeOptionsFromAnnotation(cmd) diff --git a/pkg/cmdutil/templateflags.go b/pkg/cmdutil/templateflags.go index 611cf096c..0c2585a9e 100644 --- a/pkg/cmdutil/templateflags.go +++ b/pkg/cmdutil/templateflags.go @@ -9,12 +9,17 @@ import ( "regexp" "strings" - "github.com/reubenmiller/go-c8y-cli/v2/pkg/config" "github.com/reubenmiller/go-c8y-cli/v2/pkg/flags" "github.com/reubenmiller/go-c8y-cli/v2/pkg/pathresolver" "github.com/spf13/cobra" ) +var NamespaceSeparator = "::" + +func BuildTemplatePath(namespace, name string) string { + return fmt.Sprintf("%s%s%s", namespace, NamespaceSeparator, name) +} + type TemplatePathResolver struct { Paths []string } @@ -29,32 +34,50 @@ func matchFilePath(paths []string, pattern string, extensions []string, ignoreDi return pattern, nil } + sourcePattern := "" + if a, b, ok := strings.Cut(pattern, NamespaceSeparator); ok { + sourcePattern = a + pattern = b + } + // abort if template path does not exist validPaths := []string{} for _, sourceDir := range paths { + sourceName := "" + sourcePath := sourceDir + if a, b, ok := strings.Cut(sourceDir, NamespaceSeparator); ok { + sourceName = a + sourcePath = b + } + + if sourcePattern != "" { + if sourceMatch, _ := filepath.Match(sourceName, sourcePattern); !sourceMatch { + continue + } + } - if stat, err := os.Stat(sourceDir); err == nil && stat.IsDir() { + if stat, err := os.Stat(sourcePath); err == nil && stat.IsDir() { // path exists under template path (return early) - fullPath := path.Join(sourceDir, pattern) + fullPath := path.Join(sourcePath, pattern) if stat, err := os.Stat(fullPath); err == nil && !stat.IsDir() { return fullPath, nil } - validPaths = append(validPaths, sourceDir) + validPaths = append(validPaths, sourcePath) } } if len(validPaths) == 0 { - return "", fmt.Errorf("Template paths do not exist. %v", paths) + return "", fmt.Errorf("template paths do not exist. %v", paths) } // try to resolve path in nested - names, err := pathresolver.ResolvePaths(paths, pattern, extensions, ignoreDir) + names, err := pathresolver.ResolvePaths(validPaths, pattern, extensions, ignoreDir) if err != nil { return "", err } if len(names) == 0 { - return "", fmt.Errorf("No matching files found") + return "", fmt.Errorf("no matching files found") } return names[0], nil @@ -63,20 +86,16 @@ func matchFilePath(paths []string, pattern string, extensions []string, ignoreDi // WithTemplateFlag add template flag with completion func (f *Factory) WithTemplateFlag(cmd *cobra.Command) flags.Option { return func(cmd *cobra.Command) *cobra.Command { - cfg, err := f.Config() - if err != nil { - return nil - } + // cfg, err := f.Config() + // if err != nil { + // return nil + // } cmd.Flags().String(flags.FlagDataTemplateName, "", "Body template") cmd.Flags().StringArray(flags.FlagDataTemplateVariablesName, []string{}, "Body template variables") _ = cmd.RegisterFlagCompletionFunc(flags.FlagDataTemplateName, func(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) { - templatePath := cfg.GetTemplatePaths() - matches, err := pathresolver.ResolvePaths(templatePath, "*"+toComplete+"*", []string{".jsonnet"}, "ignore") - for i, match := range matches { - matches[i] = filepath.Base(match) - } + matches, err := f.ResolveTemplates("*"+toComplete+"*", false) if err != nil { return []string{"jsonnet"}, cobra.ShellCompDirectiveFilterFileExt @@ -85,12 +104,13 @@ func (f *Factory) WithTemplateFlag(cmd *cobra.Command) flags.Option { }) _ = cmd.RegisterFlagCompletionFunc(flags.FlagDataTemplateVariablesName, func(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) { - templatePath := cfg.GetTemplatePaths() templateFlag, err := cmd.Flags().GetString(flags.FlagDataTemplateName) if err != nil { return nil, cobra.ShellCompDirectiveNoSpace } - matches, err := pathresolver.ResolvePaths(templatePath, templateFlag, []string{".jsonnet"}, "ignore") + + matches, err := f.ResolveTemplates(templateFlag, true) + // matches, err := pathresolver.ResolvePaths(templatePath, templateFlag, []string{".jsonnet"}, "ignore") if err != nil { return nil, cobra.ShellCompDirectiveNoSpace @@ -151,12 +171,25 @@ func (f *Factory) WithTemplateFlag(cmd *cobra.Command) flags.Option { } // WithTemplateValue get the template value using the path resolver controlled by the configuration -func WithTemplateValue(cfg *config.Config) flags.GetOption { - return flags.WithTemplateValue(flags.FlagDataTemplateName, NewTemplateResolver(cfg)) +func WithTemplateValue(factory *Factory) flags.GetOption { + return flags.WithTemplateValue(flags.FlagDataTemplateName, NewTemplateResolver(factory)) } -func NewTemplateResolver(cfg *config.Config) *TemplatePathResolver { +func NewTemplateResolver(factory *Factory) *TemplatePathResolver { + cfg, err := factory.Config() + if err != nil { + return nil + } + paths := cfg.GetTemplatePaths() + + for _, ext := range factory.ExtensionManager().List() { + path := ext.TemplatePath() + if path != "" { + paths = append(paths, BuildTemplatePath(ext.Name(), path)) + } + } + return &TemplatePathResolver{ - Paths: cfg.GetTemplatePaths(), + Paths: paths, } } diff --git a/pkg/cmdutil/templateflags_test.go b/pkg/cmdutil/templateflags_test.go index 40a2959be..8faeb796e 100644 --- a/pkg/cmdutil/templateflags_test.go +++ b/pkg/cmdutil/templateflags_test.go @@ -38,7 +38,7 @@ func Test_WithTemplateValue(t *testing.T) { cmd, body, inputIterator, - WithTemplateValue(nil), + WithTemplateValue(nil, nil), flags.WithTemplateVariablesValue(), ) diff --git a/pkg/cmdutil/viewcompletion.go b/pkg/cmdutil/viewcompletion.go index dbe71b052..9fa351358 100644 --- a/pkg/cmdutil/viewcompletion.go +++ b/pkg/cmdutil/viewcompletion.go @@ -31,7 +31,11 @@ func WithViewCompletion(flagName string, dataviewFunc func() (*dataview.DataView config.ViewsAuto + "\tAuto detect view", } for _, item := range items { - values = append(values, fmt.Sprintf("%s\t%v | file: %s", item.Name, item.Columns, item.FileName)) + if item.Extension != "" { + values = append(values, fmt.Sprintf("%s%s%s\t%v | extension: %s, file: %s, ", item.Extension, NamespaceSeparator, item.Name, item.Columns, item.Extension, item.FileName)) + } else { + values = append(values, fmt.Sprintf("%s\t%v | file: %s", item.Name, item.Columns, item.FileName)) + } } return values, cobra.ShellCompDirectiveNoFileComp }) diff --git a/pkg/completion/application.go b/pkg/completion/application.go index 601c06675..e7456ab57 100644 --- a/pkg/completion/application.go +++ b/pkg/completion/application.go @@ -1,7 +1,6 @@ package completion import ( - "context" "fmt" "strings" @@ -20,7 +19,7 @@ func WithApplication(flagName string, clientFunc func() (*c8y.Client, error)) Op return []string{err.Error()}, cobra.ShellCompDirectiveDefault } items, _, err := client.Application.GetApplications( - context.Background(), + WithDisabledDryRunContext(client), &c8y.ApplicationOptions{ PaginationOptions: *c8y.NewPaginationOptions(2000), }, @@ -52,7 +51,7 @@ func WithApplicationContext(flagName string, clientFunc func() (*c8y.Client, err return []string{err.Error()}, cobra.ShellCompDirectiveDefault } items, _, err := client.Application.GetApplications( - context.Background(), + WithDisabledDryRunContext(client), &c8y.ApplicationOptions{ PaginationOptions: *c8y.NewPaginationOptions(2000), }, @@ -84,7 +83,7 @@ func WithHostedApplication(flagName string, clientFunc func() (*c8y.Client, erro return []string{err.Error()}, cobra.ShellCompDirectiveDefault } items, _, err := client.Application.GetApplications( - context.Background(), + WithDisabledDryRunContext(client), &c8y.ApplicationOptions{ PaginationOptions: *c8y.NewPaginationOptions(2000), }, @@ -114,6 +113,20 @@ func WithHostedApplication(flagName string, clientFunc func() (*c8y.Client, erro values = append(values, fmt.Sprintf("%s\t%s | id: %s", item.Name, item.Type, item.ID)) } } + + // If no results, then included applications that are not owned by the current tenant + if len(values) == 0 { + for _, item := range items.Applications { + if !strings.EqualFold(item.Type, "HOSTED") { + continue + } + + if toComplete == "" || MatchString(pattern, item.Name) || MatchString(pattern, item.ID) { + values = append(values, fmt.Sprintf("%s\t%s | id: %s", item.Name, item.Type, item.ID)) + } + } + } + return values, cobra.ShellCompDirectiveNoFileComp }) return cmd @@ -129,7 +142,7 @@ func WithMicroservice(flagName string, clientFunc func() (*c8y.Client, error)) O return []string{err.Error()}, cobra.ShellCompDirectiveDefault } items, _, err := client.Application.GetApplications( - context.Background(), + WithDisabledDryRunContext(client), &c8y.ApplicationOptions{ PaginationOptions: *c8y.NewPaginationOptions(2000), }, @@ -170,10 +183,13 @@ func WithMicroserviceLoggers(flagName string, flagNameMicroserviceName string, c } values := []string{} - resp, err := client.SendRequest(context.Background(), c8y.RequestOptions{ - Method: "GET", - Path: "/service/" + microserviceName + "/loggers", - }) + resp, err := client.SendRequest( + WithDisabledDryRunContext(client), + c8y.RequestOptions{ + Method: "GET", + Path: "/service/" + microserviceName + "/loggers", + }, + ) if err != nil { values := []string{fmt.Sprintf("error. %s", err)} @@ -197,7 +213,7 @@ func WithMicroserviceLoggers(flagName string, flagNameMicroserviceName string, c func getMicroserviceByName(client *c8y.Client, name string) (string, error) { apps, _, err := client.Application.GetApplicationsByName( - context.Background(), + WithDisabledDryRunContext(client), name, &c8y.ApplicationOptions{ Type: c8y.ApplicationTypeMicroservice, @@ -241,7 +257,7 @@ func getMicroserviceInstances(cmd *cobra.Command, flagApplicationID string, clie pattern := "*" + toComplete + "*" items, _, err := client.Inventory.GetManagedObjects( - context.Background(), + WithDisabledDryRunContext(client), &c8y.ManagedObjectOptions{ Type: "c8y_Application_" + deviceID, PaginationOptions: *c8y.NewPaginationOptions(1), diff --git a/pkg/completion/certificate.go b/pkg/completion/certificate.go index 30b17ea6f..a6cde08e0 100644 --- a/pkg/completion/certificate.go +++ b/pkg/completion/certificate.go @@ -1,7 +1,6 @@ package completion import ( - "context" "fmt" "github.com/reubenmiller/go-c8y/pkg/c8y" @@ -17,7 +16,7 @@ func WithDeviceCertificate(flagName string, clientFunc func() (*c8y.Client, erro return []string{err.Error()}, cobra.ShellCompDirectiveDefault } items, _, err := client.DeviceCertificate.GetCertificates( - context.Background(), + WithDisabledDryRunContext(client), &c8y.DeviceCertificateCollectionOptions{ PaginationOptions: *c8y.NewPaginationOptions(100), }, diff --git a/pkg/completion/completion.go b/pkg/completion/completion.go index 8d31a751d..1a330f1d1 100644 --- a/pkg/completion/completion.go +++ b/pkg/completion/completion.go @@ -1,9 +1,18 @@ package completion import ( + "context" + + "github.com/reubenmiller/go-c8y/pkg/c8y" "github.com/spf13/cobra" ) +func WithDisabledDryRunContext(c *c8y.Client) context.Context { + return c.Context.CommonOptions(c8y.CommonOptions{ + DryRun: false, + }) +} + // GetFlagStringValues get string slice from either a string slice or string flag func GetFlagStringValues(cmd *cobra.Command, name string) ([]string, error) { items, err := cmd.Flags().GetStringSlice(name) diff --git a/pkg/completion/configuration.go b/pkg/completion/configuration.go index 04a513370..4053a0002 100644 --- a/pkg/completion/configuration.go +++ b/pkg/completion/configuration.go @@ -1,7 +1,6 @@ package completion import ( - "context" "fmt" "github.com/reubenmiller/go-c8y/pkg/c8y" @@ -23,7 +22,7 @@ func WithConfiguration(flagName string, clientFunc func() (*c8y.Client, error)) PaginationOptions: *c8y.NewPaginationOptions(100), } items, _, err := client.Inventory.GetManagedObjects( - context.Background(), + WithDisabledDryRunContext(client), opt, ) diff --git a/pkg/completion/device.go b/pkg/completion/device.go index 3a68f0fcf..db3e1e194 100644 --- a/pkg/completion/device.go +++ b/pkg/completion/device.go @@ -1,7 +1,6 @@ package completion import ( - "context" "fmt" "github.com/reubenmiller/go-c8y/pkg/c8y" @@ -19,7 +18,7 @@ func WithDevice(flagName string, clientFunc func() (*c8y.Client, error)) Option pattern := "*" + toComplete + "*" items, _, err := client.Inventory.GetDevicesByName( - context.Background(), + WithDisabledDryRunContext(client), pattern, c8y.NewPaginationOptions(100), ) @@ -55,7 +54,7 @@ func WithAgent(flagName string, clientFunc func() (*c8y.Client, error)) Option { PaginationOptions: *c8y.NewPaginationOptions(100), } items, _, err := client.Inventory.GetManagedObjects( - context.Background(), + WithDisabledDryRunContext(client), opt, ) diff --git a/pkg/completion/deviceProfile.go b/pkg/completion/deviceProfile.go index b8e12ce47..96762b320 100644 --- a/pkg/completion/deviceProfile.go +++ b/pkg/completion/deviceProfile.go @@ -1,7 +1,6 @@ package completion import ( - "context" "fmt" "github.com/reubenmiller/go-c8y/pkg/c8y" @@ -23,7 +22,7 @@ func WithDeviceProfile(flagName string, clientFunc func() (*c8y.Client, error)) PaginationOptions: *c8y.NewPaginationOptions(100), } items, _, err := client.Inventory.GetManagedObjects( - context.Background(), + WithDisabledDryRunContext(client), opt, ) diff --git a/pkg/completion/deviceService.go b/pkg/completion/deviceService.go index cc714e81d..7fd9dffe6 100644 --- a/pkg/completion/deviceService.go +++ b/pkg/completion/deviceService.go @@ -1,7 +1,6 @@ package completion import ( - "context" "fmt" "github.com/reubenmiller/go-c8y/pkg/c8y" @@ -45,7 +44,7 @@ func WithDeviceService(flagService string, flagDevice string, clientFunc func() } else { // lookup via name items, _, err := client.Inventory.GetDevicesByName( - context.Background(), + WithDisabledDryRunContext(client), deviceName, c8y.NewPaginationOptions(100), ) @@ -62,7 +61,7 @@ func WithDeviceService(flagService string, flagDevice string, clientFunc func() query := fmt.Sprintf("type eq 'c8y_Service' and name eq '%s' and bygroupid(%s)", serviceNamePattern, deviceID) items, _, err := client.Inventory.GetManagedObjects( - context.Background(), + WithDisabledDryRunContext(client), &c8y.ManagedObjectOptions{ Query: query, PaginationOptions: *c8y.NewPaginationOptions(100), diff --git a/pkg/completion/devicegroup.go b/pkg/completion/devicegroup.go index 5923eaa59..96ca11195 100644 --- a/pkg/completion/devicegroup.go +++ b/pkg/completion/devicegroup.go @@ -1,7 +1,6 @@ package completion import ( - "context" "fmt" "github.com/reubenmiller/go-c8y/pkg/c8y" @@ -23,7 +22,7 @@ func WithDeviceGroup(flagName string, clientFunc func() (*c8y.Client, error)) Op PaginationOptions: *c8y.NewPaginationOptions(100), } items, _, err := client.Inventory.GetManagedObjects( - context.Background(), + WithDisabledDryRunContext(client), opt, ) diff --git a/pkg/completion/deviceregistration.go b/pkg/completion/deviceregistration.go index c53b85b52..89793059e 100644 --- a/pkg/completion/deviceregistration.go +++ b/pkg/completion/deviceregistration.go @@ -1,7 +1,6 @@ package completion import ( - "context" "fmt" "github.com/reubenmiller/go-c8y/pkg/c8y" @@ -19,7 +18,7 @@ func WithDeviceRegistrationRequest(flagName string, clientFunc func() (*c8y.Clie pattern := "*" + toComplete + "*" items, _, err := client.DeviceCredentials.GetNewDeviceRequests( - context.Background(), + WithDisabledDryRunContext(client), &c8y.NewDeviceRequestOptions{ PaginationOptions: *c8y.NewPaginationOptions(100), }, diff --git a/pkg/completion/external.go b/pkg/completion/external.go new file mode 100644 index 000000000..7355f3c77 --- /dev/null +++ b/pkg/completion/external.go @@ -0,0 +1,45 @@ +package completion + +import ( + "log" + "os" + "os/exec" + "strings" + + "github.com/reubenmiller/go-c8y-cli/v2/pkg/extexec" + "github.com/spf13/cobra" +) + +// WithExternalCompletion completion by executing an external command or another c8y command +func WithExternalCompletion(flagName string, externalCommand []string) Option { + return func(cmd *cobra.Command) *cobra.Command { + _ = cmd.RegisterFlagCompletionFunc(flagName, func(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) { + + log.Printf("Completing external flag. args=%v, completion_cmd=%v, toComplete=%s\n", os.Args, externalCommand, toComplete) + output, err := extexec.ExecuteExternalCommand(toComplete, externalCommand) + + if err != nil { + if exiterr, ok := err.(*exec.ExitError); ok { + log.Printf("stderr: %s", exiterr.Stderr) + } else { + log.Printf("output: %s, %s", output, err) + } + + return []string{err.Error()}, cobra.ShellCompDirectiveNoFileComp + } + + log.Printf("Output: %s", output) + // TODO: Use tsv instead of csv output + // TODO: Add support for field annotations on the descriptions + // eg. // rowParts = append(rowParts, fmt.Sprintf("%s: %s", fields[i], col)) + options := []string{} + for _, row := range strings.Split(string(output), "\n") { + if len(row) > 0 { + options = append(options, row) + } + } + return options, cobra.ShellCompDirectiveNoFileComp + }) + return cmd + } +} diff --git a/pkg/completion/firmware.go b/pkg/completion/firmware.go index 1db2a553e..23a6e7ce6 100644 --- a/pkg/completion/firmware.go +++ b/pkg/completion/firmware.go @@ -1,7 +1,6 @@ package completion import ( - "context" "fmt" "github.com/reubenmiller/go-c8y/pkg/c8y" @@ -19,7 +18,7 @@ func WithFirmware(flagName string, clientFunc func() (*c8y.Client, error)) Optio pattern := "*" + toComplete + "*" items, _, err := client.Firmware.GetFirmwareByName( - context.Background(), + WithDisabledDryRunContext(client), pattern, c8y.NewPaginationOptions(100), ) diff --git a/pkg/completion/firmwareVersion.go b/pkg/completion/firmwareVersion.go index 525cceb5b..98c2fd46a 100644 --- a/pkg/completion/firmwareVersion.go +++ b/pkg/completion/firmwareVersion.go @@ -1,7 +1,6 @@ package completion import ( - "context" "fmt" "github.com/reubenmiller/go-c8y-cli/v2/pkg/c8ydata" @@ -46,7 +45,7 @@ func WithFirmwareVersion(flagVersion string, flagNameFirmware string, clientFunc } else { // Lookup by name packages, _, err := client.Inventory.GetManagedObjects( - context.Background(), + WithDisabledDryRunContext(client), &c8y.ManagedObjectOptions{ Query: fmt.Sprintf("$filter=(type eq '%s') and name eq '%s' $orderby=name,creationTime", "c8y_Firmware", firmwareName), }, @@ -60,7 +59,7 @@ func WithFirmwareVersion(flagVersion string, flagNameFirmware string, clientFunc } items, _, err := client.Inventory.GetManagedObjects( - context.Background(), + WithDisabledDryRunContext(client), opt, ) diff --git a/pkg/completion/firmwareVersionPatch.go b/pkg/completion/firmwareVersionPatch.go index 19c13e6b7..df3773052 100644 --- a/pkg/completion/firmwareVersionPatch.go +++ b/pkg/completion/firmwareVersionPatch.go @@ -1,7 +1,6 @@ package completion import ( - "context" "fmt" "github.com/reubenmiller/go-c8y-cli/v2/pkg/c8ydata" @@ -46,7 +45,7 @@ func WithFirmwarePatch(flagVersion string, flagNameFirmware string, clientFunc f } else { // Lookup by name packages, _, err := client.Inventory.GetManagedObjects( - context.Background(), + WithDisabledDryRunContext(client), &c8y.ManagedObjectOptions{ Query: fmt.Sprintf("(type eq '%s') and name eq '%s'", "c8y_Firmware", firmwareName), }, @@ -60,7 +59,7 @@ func WithFirmwarePatch(flagVersion string, flagNameFirmware string, clientFunc f } items, _, err := client.Inventory.GetManagedObjects( - context.Background(), + WithDisabledDryRunContext(client), opt, ) diff --git a/pkg/completion/measurements.go b/pkg/completion/measurements.go index 0068a37c4..6d29ac7fa 100644 --- a/pkg/completion/measurements.go +++ b/pkg/completion/measurements.go @@ -1,7 +1,6 @@ package completion import ( - "context" "errors" "strings" @@ -27,7 +26,7 @@ func getSupportedSeries(cmd *cobra.Command, flagNameDevice string, client *c8y.C if !c8ydata.IsID(deviceID) { matchingDevices, _, err := client.Inventory.GetDevicesByName( - context.Background(), + WithDisabledDryRunContext(client), deviceID, c8y.NewPaginationOptions(1), ) @@ -42,7 +41,7 @@ func getSupportedSeries(cmd *cobra.Command, flagNameDevice string, client *c8y.C pattern := "*" + toComplete + "*" items, _, err := client.Inventory.GetSupportedSeries( - context.Background(), + WithDisabledDryRunContext(client), deviceID, ) diff --git a/pkg/completion/notification2.go b/pkg/completion/notification2.go index 2a6c57ed5..4402bbb22 100644 --- a/pkg/completion/notification2.go +++ b/pkg/completion/notification2.go @@ -1,7 +1,6 @@ package completion import ( - "context" "fmt" "strings" @@ -18,7 +17,7 @@ func WithNotification2SubscriptionName(flagName string, clientFunc func() (*c8y. return []string{err.Error()}, cobra.ShellCompDirectiveError } items, _, err := client.Notification2.GetSubscriptions( - context.Background(), + WithDisabledDryRunContext(client), &c8y.Notification2SubscriptionCollectionOptions{ PaginationOptions: *c8y.NewPaginationOptions(2000), }, @@ -73,7 +72,7 @@ func WithNotification2SubscriptionId(flagName string, clientFunc func() (*c8y.Cl return []string{err.Error()}, cobra.ShellCompDirectiveError } items, _, err := client.Notification2.GetSubscriptions( - context.Background(), + WithDisabledDryRunContext(client), &c8y.Notification2SubscriptionCollectionOptions{ PaginationOptions: *c8y.NewPaginationOptions(2000), }, diff --git a/pkg/completion/role.go b/pkg/completion/role.go index 6e99217a4..6fd80f158 100644 --- a/pkg/completion/role.go +++ b/pkg/completion/role.go @@ -1,7 +1,6 @@ package completion import ( - "context" "fmt" "github.com/reubenmiller/go-c8y/pkg/c8y" @@ -17,7 +16,7 @@ func WithUserRole(flagName string, clientFunc func() (*c8y.Client, error)) Optio return []string{err.Error()}, cobra.ShellCompDirectiveDefault } items, _, err := client.User.GetRoles( - context.Background(), + WithDisabledDryRunContext(client), &c8y.RoleOptions{ PaginationOptions: *c8y.NewPaginationOptions(2000), }, diff --git a/pkg/completion/smartgroup.go b/pkg/completion/smartgroup.go index 0ffcc7150..4f1278e37 100644 --- a/pkg/completion/smartgroup.go +++ b/pkg/completion/smartgroup.go @@ -1,7 +1,6 @@ package completion import ( - "context" "fmt" "github.com/reubenmiller/go-c8y/pkg/c8y" @@ -23,7 +22,7 @@ func WithSmartGroup(flagName string, clientFunc func() (*c8y.Client, error)) Opt PaginationOptions: *c8y.NewPaginationOptions(100), } items, _, err := client.Inventory.GetManagedObjects( - context.Background(), + WithDisabledDryRunContext(client), opt, ) diff --git a/pkg/completion/software.go b/pkg/completion/software.go index 2dbea9325..7e5579567 100644 --- a/pkg/completion/software.go +++ b/pkg/completion/software.go @@ -1,7 +1,6 @@ package completion import ( - "context" "fmt" "github.com/reubenmiller/go-c8y/pkg/c8y" @@ -19,7 +18,7 @@ func WithSoftware(flagName string, clientFunc func() (*c8y.Client, error)) Optio pattern := "*" + toComplete + "*" items, _, err := client.Software.GetSoftwareByName( - context.Background(), + WithDisabledDryRunContext(client), pattern, c8y.NewPaginationOptions(100), ) diff --git a/pkg/completion/softwareVersion.go b/pkg/completion/softwareVersion.go index 6ecac59c7..f5027ee23 100644 --- a/pkg/completion/softwareVersion.go +++ b/pkg/completion/softwareVersion.go @@ -1,7 +1,6 @@ package completion import ( - "context" "fmt" "github.com/reubenmiller/go-c8y/pkg/c8y" @@ -31,7 +30,7 @@ func WithSoftwareVersion(flagVersion string, flagNameSoftware string, clientFunc } items, _, err := client.Software.GetSoftwareVersionsByName( - context.Background(), + WithDisabledDryRunContext(client), softwareName, versionPattern, true, diff --git a/pkg/completion/systemoption.go b/pkg/completion/systemoption.go index dd9f526e1..f481e5f22 100644 --- a/pkg/completion/systemoption.go +++ b/pkg/completion/systemoption.go @@ -1,7 +1,6 @@ package completion import ( - "context" "fmt" "github.com/reubenmiller/go-c8y/pkg/c8y" @@ -19,7 +18,7 @@ func WithSystemOptionCategory(flagName string, clientFunc func() (*c8y.Client, e pattern := "*" + toComplete + "*" items, _, err := client.TenantOptions.GetSystemOptions( - context.Background(), + WithDisabledDryRunContext(client), c8y.NewPaginationOptions(200), ) @@ -59,7 +58,7 @@ func WithSystemOptionKey(flagName string, flagNameCategory string, clientFunc fu pattern := "*" + toComplete + "*" items, _, err := client.TenantOptions.GetSystemOptions( - context.Background(), + WithDisabledDryRunContext(client), c8y.NewPaginationOptions(200), ) diff --git a/pkg/completion/tenant.go b/pkg/completion/tenant.go index b5ab7ce13..ca2ff1211 100644 --- a/pkg/completion/tenant.go +++ b/pkg/completion/tenant.go @@ -1,7 +1,6 @@ package completion import ( - "context" "fmt" "strings" @@ -18,7 +17,7 @@ func WithTenantID(flagName string, clientFunc func() (*c8y.Client, error)) Optio return []string{err.Error()}, cobra.ShellCompDirectiveDefault } tenants, _, err := client.Tenant.GetTenants( - context.Background(), + WithDisabledDryRunContext(client), c8y.NewPaginationOptions(20), ) diff --git a/pkg/completion/tenantoption.go b/pkg/completion/tenantoption.go index fd852f06f..aff40642e 100644 --- a/pkg/completion/tenantoption.go +++ b/pkg/completion/tenantoption.go @@ -1,7 +1,6 @@ package completion import ( - "context" "fmt" "github.com/reubenmiller/go-c8y/pkg/c8y" @@ -19,7 +18,7 @@ func WithTenantOptionCategory(flagName string, clientFunc func() (*c8y.Client, e pattern := "*" + toComplete + "*" items, _, err := client.TenantOptions.GetOptions( - context.Background(), + WithDisabledDryRunContext(client), c8y.NewPaginationOptions(200), ) @@ -59,7 +58,7 @@ func WithTenantOptionKey(flagName string, flagNameCategory string, clientFunc fu pattern := "*" + toComplete + "*" items, _, err := client.TenantOptions.GetOptions( - context.Background(), + WithDisabledDryRunContext(client), c8y.NewPaginationOptions(200), ) diff --git a/pkg/completion/user.go b/pkg/completion/user.go index cfaad0a1b..381cfe01d 100644 --- a/pkg/completion/user.go +++ b/pkg/completion/user.go @@ -1,7 +1,6 @@ package completion import ( - "context" "fmt" "strings" @@ -19,7 +18,7 @@ func WithUser(flagName string, clientFunc func() (*c8y.Client, error)) Option { } items, _, err := client.User.GetUsers( - context.Background(), + WithDisabledDryRunContext(client), &c8y.UserOptions{ PaginationOptions: *c8y.NewPaginationOptions(2000), }, diff --git a/pkg/completion/usergroup.go b/pkg/completion/usergroup.go index 6593ac7ab..6122c0b35 100644 --- a/pkg/completion/usergroup.go +++ b/pkg/completion/usergroup.go @@ -1,7 +1,6 @@ package completion import ( - "context" "fmt" "github.com/reubenmiller/go-c8y/pkg/c8y" @@ -18,7 +17,7 @@ func WithUserGroup(flagName string, clientFunc func() (*c8y.Client, error)) Opti } items, _, err := client.User.GetGroups( - context.Background(), + WithDisabledDryRunContext(client), &c8y.GroupOptions{ PaginationOptions: *c8y.NewPaginationOptions(100), }, diff --git a/pkg/config/cliConfiguration.go b/pkg/config/cliConfiguration.go index d8c78a7b5..df408483b 100644 --- a/pkg/config/cliConfiguration.go +++ b/pkg/config/cliConfiguration.go @@ -302,6 +302,11 @@ const ( // SettingsBrowser default browser SettingsBrowser = "settings.browser" + + // Extensions + SettingsExtensionDataDir = "settings.extensions.datadir" + SettingsExtensionDefaultHost = "settings.extensions.defaultHost" + SettingsExtensionDefaultUsername = "settings.extensions.defaultUsername" ) var ( @@ -457,6 +462,10 @@ func (c *Config) bindSettings() { WithBindEnv(SettingsCacheDir, filepath.Join(os.TempDir(), "go-c8y-cli-cache")), WithBindEnv(SettingsBrowser, ""), + + // Extensions + WithBindEnv(SettingsExtensionDataDir, ""), + WithBindEnv(SettingsExtensionDefaultHost, "github.com"), ) if err != nil { @@ -928,8 +937,10 @@ func (c *Config) DecryptSession() error { } // CommonAliases Get common aliases from the global configuration file +// deprecated in favor of extensions func (c *Config) CommonAliases() map[string]string { - return c.viper.GetStringMapString(SettingsCommonAliases) + return map[string]string{} + // return c.viper.GetStringMapString(SettingsCommonAliases) } // Aliases get aliases configured in the current session @@ -1212,8 +1223,9 @@ func (c *Config) StorePassword() bool { } // GetTemplatePaths template folders where the template files are located -func (c *Config) GetTemplatePaths() (paths []string) { +func (c *Config) GetTemplatePaths() []string { // Prefer custom path over default path + paths := make([]string, 0) paths = append(paths, c.GetPathSlice(SettingsTemplateCustomPaths)...) paths = append(paths, c.GetPathSlice(SettingsTemplatePath)...) return paths @@ -1284,6 +1296,16 @@ func (c *Config) IsCSVOutput() bool { return format == OutputCSV || format == OutputCSVWithHeader } +func (c *Config) IsTSVOutput() bool { + format := c.GetOutputFormat() + return format == OutputTSV +} + +func (c *Config) IsCompletionOutput() bool { + format := c.GetOutputFormat() + return format == OutputCompletion +} + // IsResponseOutput check if raw server response should be used func (c *Config) IsResponseOutput() bool { return c.GetOutputFormat() == OutputServerResponse @@ -1331,9 +1353,9 @@ func (c *Config) GetConfigPath() string { // GetViewPaths get list of view paths func (c *Config) GetViewPaths() []string { - viewPaths := c.GetPathSlice(SettingsViewsCommonPaths) - viewPaths = append(viewPaths, c.GetPathSlice(SettingsViewsCustomPaths)...) - return viewPaths + paths := c.GetPathSlice(SettingsViewsCommonPaths) + paths = append(paths, c.GetPathSlice(SettingsViewsCustomPaths)...) + return paths } // GetJSONFilter get json filter to be applied to the output @@ -1409,6 +1431,23 @@ func (c *Config) Browser() string { return c.viper.GetString(SettingsBrowser) } +// Get Extension Data Directory +func (c *Config) ExtensionsDataDir() string { + dir := c.viper.GetString(SettingsExtensionDataDir) + if dir == "" { + dir = c.GetSessionHomeDir() + } + return filepath.Join(dir, "extensions") +} + +func (c *Config) DefaultHost() string { + return c.viper.GetString(SettingsExtensionDefaultHost) +} + +func (c *Config) ExtensionDefaultUsername() string { + return c.viper.GetString(SettingsExtensionDefaultUsername) +} + // GetJSONSelect get json properties to be selected from the output. Only the given properties will be returned func (c *Config) GetJSONSelect() []string { // Note: select is stored as an cobra Array String, which add special formating of values. @@ -1444,6 +1483,8 @@ func (c *Config) GetOutputCommonOptions(cmd *cobra.Command) (CommonCommandOption // Filters and selectors filters := jsonfilter.NewJSONFilters(c.Logger) filters.AsCSV = c.IsCSVOutput() + filters.AsTSV = c.IsTSVOutput() + filters.AsCompletionFormat = c.IsCompletionOutput() filters.Flatten = c.FlattenJSON() filters.Pluck = c.GetJSONSelect() if err := filters.AddRawFilters(c.GetJSONFilter()); err != nil { diff --git a/pkg/config/outputformat.go b/pkg/config/outputformat.go index 64a8cc214..af324ed7b 100644 --- a/pkg/config/outputformat.go +++ b/pkg/config/outputformat.go @@ -14,6 +14,12 @@ const ( // OutputCSV csv output OutputCSV + // OutputTSV tsv output + OutputTSV + + // OutputCompletion completion output format, e.g. {value}\t{key1}: {value1} | {key2}: {value2} + OutputCompletion + // OutputCSVWithHeader csv output with header OutputCSVWithHeader @@ -26,6 +32,8 @@ func (f OutputFormat) String() string { OutputJSON: "json", OutputTable: "table", OutputCSV: "csv", + OutputTSV: "tsv", + OutputCompletion: "completion", OutputCSVWithHeader: "csvheader", OutputServerResponse: "serverresponse", } @@ -41,6 +49,8 @@ func (f OutputFormat) FromString(name string) OutputFormat { "json": OutputJSON, "table": OutputTable, "csv": OutputCSV, + "tsv": OutputTSV, + "completion": OutputCompletion, "csvheader": OutputCSVWithHeader, "serverresponse": OutputServerResponse, } diff --git a/pkg/console/console.go b/pkg/console/console.go index bd7f24a0a..2719eda14 100644 --- a/pkg/console/console.go +++ b/pkg/console/console.go @@ -82,6 +82,10 @@ func (c *Console) IsCSV() bool { return c.Format == config.OutputCSV || c.Format == config.OutputCSVWithHeader } +func (c *Console) IsTextOutput() bool { + return c.Format == config.OutputCSV || c.Format == config.OutputCSVWithHeader || c.Format == config.OutputTSV || c.Format == config.OutputCompletion +} + // WithCSVHeader returns true if the csv output should include a header func (c *Console) WithCSVHeader() bool { return c.Format == config.OutputCSVWithHeader diff --git a/pkg/dataview/dataview.go b/pkg/dataview/dataview.go index ddfd1eb16..ac6da9f0f 100644 --- a/pkg/dataview/dataview.go +++ b/pkg/dataview/dataview.go @@ -2,8 +2,10 @@ package dataview import ( "encoding/json" + "fmt" "io/fs" "io/ioutil" + "net/http" "os" "path/filepath" "regexp" @@ -16,16 +18,28 @@ import ( "github.com/tidwall/gjson" ) +var NamespaceSeparator = "::" + // Definition contains the view definition of when to use a specific view type Definition struct { - FileName string `json:"-"` - Name string `json:"name,omitempty"` - Priority int `json:"priority,omitempty"` - Fragments []string `json:"fragments,omitempty"` - Type string `json:"type,omitempty"` - ContentType string `json:"contentType,omitempty"` - Self string `json:"self,omitempty"` - Columns []string `json:"columns,omitempty"` + FileName string `json:"-"` + Extension string `json:"-"` + Name string `json:"name,omitempty"` + Priority int `json:"priority,omitempty"` + Fragments []string `json:"fragments,omitempty"` + Type string `json:"type,omitempty"` + RequestPath string `json:"requestPath,omitempty"` + RequestMethod string `json:"requestMethod,omitempty"` + ContentType string `json:"contentType,omitempty"` + Self string `json:"self,omitempty"` + Columns []string `json:"columns,omitempty"` +} + +func (d *Definition) FQDN() string { + if d.Extension != "" { + return fmt.Sprintf("%s%s%s", d.Extension, NamespaceSeparator, d.Name) + } + return d.Name } // DefinitionCollection collection of view definitions @@ -75,8 +89,18 @@ func (v *DataView) LoadDefinitions() error { for _, path := range v.Paths { v.Logger.Debugf("Current view path: %s", path) + extName := "" + if strings.Contains(path, NamespaceSeparator) { + if b, a, ok := strings.Cut(path, NamespaceSeparator); ok { + extName = b + path = a + } + } + if stat, err := os.Stat(path); err != nil { - v.Logger.Debugf("Skipping view path because it does not exist. path=%s, error=%s", path, err) + if extName == "" { + v.Logger.Debugf("Skipping view path because it does not exist. path=%s, error=%s", path, err) + } continue } else if !stat.IsDir() { v.Logger.Debugf("Skipping view path because it is not a folder. path=%s", path) @@ -102,14 +126,20 @@ func (v *DataView) LoadDefinitions() error { return err } - v.Logger.Debugf("Found view definition: %s", d.Name()) + if extName != "" { + v.Logger.Debugf("Found view definition: %s | extension: %s", d.Name(), extName) + } else { + v.Logger.Debugf("Found view definition: %s", d.Name()) + } viewDefinition := &DefinitionCollection{} if err := json.Unmarshal(contents, &viewDefinition); err != nil { v.Logger.Warnf("Could not load view definitions. %s", err) - return err + // do not prevent walking other folders + return nil } for i := range viewDefinition.Definitions { viewDefinition.Definitions[i].FileName = d.Name() + viewDefinition.Definitions[i].Extension = extName } definitions = append(definitions, viewDefinition.Definitions...) } @@ -141,7 +171,7 @@ func (v *DataView) GetViewByName(pattern string) ([]string, error) { for _, definition := range v.GetDefinitions() { - if match, _ := matcher.MatchWithWildcards(definition.Name, pattern); match { + if match, _ := matcher.MatchWithWildcards(definition.FQDN(), pattern); match { matchingDefinition = &definition break } @@ -164,7 +194,7 @@ func (v *DataView) GetViews(pattern string) ([]Definition, error) { matches := []Definition{} for _, definition := range v.GetDefinitions() { - if match, _ := matcher.MatchWithWildcards(definition.Name, pattern); match { + if match, _ := matcher.MatchWithWildcards(definition.FQDN(), pattern); match { matches = append(matches, definition) } } @@ -192,12 +222,20 @@ func (v *DataView) ClearActiveView() { v.ActiveView = nil } -func (v *DataView) GetView(data *gjson.Result, contentType ...string) ([]string, error) { +type ViewData struct { + ResponseBody *gjson.Result + ContentType string + Response *http.Response + Request *http.Request +} + +func (v *DataView) GetView(r *ViewData) ([]string, error) { if view := v.GetActiveView(); view != nil { v.Logger.Debugf("Using already active view") return view.Columns, nil } + data := r.ResponseBody err := v.LoadDefinitions() if err != nil { return nil, err @@ -233,8 +271,8 @@ func (v *DataView) GetView(data *gjson.Result, contentType ...string) ([]string, } } - if len(contentType) > 0 { - if match, err := regexp.MatchString("(?i)"+definition.ContentType, contentType[0]); err == nil && !match { + if definition.ContentType != "" { + if match, err := regexp.MatchString("(?i)"+definition.ContentType, r.ContentType); err == nil && !match { isMatch = false } } @@ -249,6 +287,25 @@ func (v *DataView) GetView(data *gjson.Result, contentType ...string) ([]string, } } + if definition.RequestPath != "" { + if r.Request == nil { + isMatch = false + } else { + if match, err := regexp.MatchString("(?i)"+definition.RequestPath, r.Request.URL.Path); err == nil && !match { + isMatch = false + } + } + } + if definition.RequestMethod != "" { + if r.Request == nil { + isMatch = false + } else { + if match, err := regexp.MatchString("(?i)"+definition.RequestMethod, r.Request.Method); err == nil && !match { + isMatch = false + } + } + } + if isMatch { matchingDefinition = &definition break diff --git a/pkg/extensions/alias_mock.go b/pkg/extensions/alias_mock.go new file mode 100644 index 000000000..9aba237a4 --- /dev/null +++ b/pkg/extensions/alias_mock.go @@ -0,0 +1,178 @@ +// Code generated by moq; DO NOT EDIT. +// github.com/matryer/moq + +package extensions + +import ( + "sync" +) + +// Ensure, that AliasMock does implement Alias. +// If this is not the case, regenerate this file with moq. +var _ Alias = &AliasMock{} + +// AliasMock is a mock implementation of Alias. +// +// func TestSomethingThatUsesAlias(t *testing.T) { +// +// // make and configure a mocked Alias +// mockedAlias := &AliasMock{ +// GetCommandFunc: func() string { +// panic("mock out the GetCommand method") +// }, +// GetDescriptionFunc: func() string { +// panic("mock out the GetDescription method") +// }, +// GetNameFunc: func() string { +// panic("mock out the GetName method") +// }, +// IsShellFunc: func() bool { +// panic("mock out the IsShell method") +// }, +// } +// +// // use mockedAlias in code that requires Alias +// // and then make assertions. +// +// } +type AliasMock struct { + // GetCommandFunc mocks the GetCommand method. + GetCommandFunc func() string + + // GetDescriptionFunc mocks the GetDescription method. + GetDescriptionFunc func() string + + // GetNameFunc mocks the GetName method. + GetNameFunc func() string + + // IsShellFunc mocks the IsShell method. + IsShellFunc func() bool + + // calls tracks calls to the methods. + calls struct { + // GetCommand holds details about calls to the GetCommand method. + GetCommand []struct { + } + // GetDescription holds details about calls to the GetDescription method. + GetDescription []struct { + } + // GetName holds details about calls to the GetName method. + GetName []struct { + } + // IsShell holds details about calls to the IsShell method. + IsShell []struct { + } + } + lockGetCommand sync.RWMutex + lockGetDescription sync.RWMutex + lockGetName sync.RWMutex + lockIsShell sync.RWMutex +} + +// GetCommand calls GetCommandFunc. +func (mock *AliasMock) GetCommand() string { + if mock.GetCommandFunc == nil { + panic("AliasMock.GetCommandFunc: method is nil but Alias.GetCommand was just called") + } + callInfo := struct { + }{} + mock.lockGetCommand.Lock() + mock.calls.GetCommand = append(mock.calls.GetCommand, callInfo) + mock.lockGetCommand.Unlock() + return mock.GetCommandFunc() +} + +// GetCommandCalls gets all the calls that were made to GetCommand. +// Check the length with: +// +// len(mockedAlias.GetCommandCalls()) +func (mock *AliasMock) GetCommandCalls() []struct { +} { + var calls []struct { + } + mock.lockGetCommand.RLock() + calls = mock.calls.GetCommand + mock.lockGetCommand.RUnlock() + return calls +} + +// GetDescription calls GetDescriptionFunc. +func (mock *AliasMock) GetDescription() string { + if mock.GetDescriptionFunc == nil { + panic("AliasMock.GetDescriptionFunc: method is nil but Alias.GetDescription was just called") + } + callInfo := struct { + }{} + mock.lockGetDescription.Lock() + mock.calls.GetDescription = append(mock.calls.GetDescription, callInfo) + mock.lockGetDescription.Unlock() + return mock.GetDescriptionFunc() +} + +// GetDescriptionCalls gets all the calls that were made to GetDescription. +// Check the length with: +// +// len(mockedAlias.GetDescriptionCalls()) +func (mock *AliasMock) GetDescriptionCalls() []struct { +} { + var calls []struct { + } + mock.lockGetDescription.RLock() + calls = mock.calls.GetDescription + mock.lockGetDescription.RUnlock() + return calls +} + +// GetName calls GetNameFunc. +func (mock *AliasMock) GetName() string { + if mock.GetNameFunc == nil { + panic("AliasMock.GetNameFunc: method is nil but Alias.GetName was just called") + } + callInfo := struct { + }{} + mock.lockGetName.Lock() + mock.calls.GetName = append(mock.calls.GetName, callInfo) + mock.lockGetName.Unlock() + return mock.GetNameFunc() +} + +// GetNameCalls gets all the calls that were made to GetName. +// Check the length with: +// +// len(mockedAlias.GetNameCalls()) +func (mock *AliasMock) GetNameCalls() []struct { +} { + var calls []struct { + } + mock.lockGetName.RLock() + calls = mock.calls.GetName + mock.lockGetName.RUnlock() + return calls +} + +// IsShell calls IsShellFunc. +func (mock *AliasMock) IsShell() bool { + if mock.IsShellFunc == nil { + panic("AliasMock.IsShellFunc: method is nil but Alias.IsShell was just called") + } + callInfo := struct { + }{} + mock.lockIsShell.Lock() + mock.calls.IsShell = append(mock.calls.IsShell, callInfo) + mock.lockIsShell.Unlock() + return mock.IsShellFunc() +} + +// IsShellCalls gets all the calls that were made to IsShell. +// Check the length with: +// +// len(mockedAlias.IsShellCalls()) +func (mock *AliasMock) IsShellCalls() []struct { +} { + var calls []struct { + } + mock.lockIsShell.RLock() + calls = mock.calls.IsShell + mock.lockIsShell.RUnlock() + return calls +} diff --git a/pkg/extensions/extension.go b/pkg/extensions/extension.go new file mode 100644 index 000000000..eb57b2f7b --- /dev/null +++ b/pkg/extensions/extension.go @@ -0,0 +1,70 @@ +package extensions + +import ( + "io" + + "github.com/reubenmiller/go-c8y-cli/v2/internal/ghrepo" +) + +type ExtTemplateType int + +const ( + GitTemplateType ExtTemplateType = 0 + GoBinTemplateType ExtTemplateType = 1 + OtherBinTemplateType ExtTemplateType = 2 +) + +//go:generate moq -rm -out extension_mock.go . Extension +type Extension interface { + Name() string // Extension Name without c8y- + Path() string // Path to executable + URL() string + CurrentVersion() string + IsPinned() bool + UpdateAvailable() bool + IsBinary() bool + IsLocal() bool + + // Extension components + TemplatePath() string + ViewPath() string + Aliases() ([]Alias, error) + Commands() ([]Command, error) +} + +//go:generate moq -rm -out alias_mock.go . Alias +type Alias interface { + GetCommand() string + GetName() string + GetDescription() string + IsShell() bool +} + +type Command interface { + Command() string + Name() string + Description() string +} + +type Template interface { + Path() string + Name() string +} + +type View interface { + Path() string + Name() string +} + +//go:generate moq -rm -out manager_mock.go . ExtensionManager +type ExtensionManager interface { + List() []Extension + Install(ghrepo.Interface, string, string) error + InstallLocal(dir string, name string) error + Upgrade(name string, force bool) error + Remove(name string) error + Dispatch(args []string, stdin io.Reader, stdout, stderr io.Writer) (bool, error) + Execute(exe string, args []string, isBinary bool, stdin io.Reader, stdout, stderr io.Writer) (bool, error) + Create(name string, tmplType ExtTemplateType) error + EnableDryRunMode() +} diff --git a/pkg/extensions/extension_mock.go b/pkg/extensions/extension_mock.go new file mode 100644 index 000000000..31093c96d --- /dev/null +++ b/pkg/extensions/extension_mock.go @@ -0,0 +1,474 @@ +// Code generated by moq; DO NOT EDIT. +// github.com/matryer/moq + +package extensions + +import ( + "sync" +) + +// Ensure, that ExtensionMock does implement Extension. +// If this is not the case, regenerate this file with moq. +var _ Extension = &ExtensionMock{} + +// ExtensionMock is a mock implementation of Extension. +// +// func TestSomethingThatUsesExtension(t *testing.T) { +// +// // make and configure a mocked Extension +// mockedExtension := &ExtensionMock{ +// AliasesFunc: func() ([]Alias, error) { +// panic("mock out the Aliases method") +// }, +// CommandsFunc: func() ([]Command, error) { +// panic("mock out the Commands method") +// }, +// CurrentVersionFunc: func() string { +// panic("mock out the CurrentVersion method") +// }, +// IsBinaryFunc: func() bool { +// panic("mock out the IsBinary method") +// }, +// IsLocalFunc: func() bool { +// panic("mock out the IsLocal method") +// }, +// IsPinnedFunc: func() bool { +// panic("mock out the IsPinned method") +// }, +// NameFunc: func() string { +// panic("mock out the Name method") +// }, +// PathFunc: func() string { +// panic("mock out the Path method") +// }, +// TemplatePathFunc: func() string { +// panic("mock out the TemplatePath method") +// }, +// URLFunc: func() string { +// panic("mock out the URL method") +// }, +// UpdateAvailableFunc: func() bool { +// panic("mock out the UpdateAvailable method") +// }, +// ViewPathFunc: func() string { +// panic("mock out the ViewPath method") +// }, +// } +// +// // use mockedExtension in code that requires Extension +// // and then make assertions. +// +// } +type ExtensionMock struct { + // AliasesFunc mocks the Aliases method. + AliasesFunc func() ([]Alias, error) + + // CommandsFunc mocks the Commands method. + CommandsFunc func() ([]Command, error) + + // CurrentVersionFunc mocks the CurrentVersion method. + CurrentVersionFunc func() string + + // IsBinaryFunc mocks the IsBinary method. + IsBinaryFunc func() bool + + // IsLocalFunc mocks the IsLocal method. + IsLocalFunc func() bool + + // IsPinnedFunc mocks the IsPinned method. + IsPinnedFunc func() bool + + // NameFunc mocks the Name method. + NameFunc func() string + + // PathFunc mocks the Path method. + PathFunc func() string + + // TemplatePathFunc mocks the TemplatePath method. + TemplatePathFunc func() string + + // URLFunc mocks the URL method. + URLFunc func() string + + // UpdateAvailableFunc mocks the UpdateAvailable method. + UpdateAvailableFunc func() bool + + // ViewPathFunc mocks the ViewPath method. + ViewPathFunc func() string + + // calls tracks calls to the methods. + calls struct { + // Aliases holds details about calls to the Aliases method. + Aliases []struct { + } + // Commands holds details about calls to the Commands method. + Commands []struct { + } + // CurrentVersion holds details about calls to the CurrentVersion method. + CurrentVersion []struct { + } + // IsBinary holds details about calls to the IsBinary method. + IsBinary []struct { + } + // IsLocal holds details about calls to the IsLocal method. + IsLocal []struct { + } + // IsPinned holds details about calls to the IsPinned method. + IsPinned []struct { + } + // Name holds details about calls to the Name method. + Name []struct { + } + // Path holds details about calls to the Path method. + Path []struct { + } + // TemplatePath holds details about calls to the TemplatePath method. + TemplatePath []struct { + } + // URL holds details about calls to the URL method. + URL []struct { + } + // UpdateAvailable holds details about calls to the UpdateAvailable method. + UpdateAvailable []struct { + } + // ViewPath holds details about calls to the ViewPath method. + ViewPath []struct { + } + } + lockAliases sync.RWMutex + lockCommands sync.RWMutex + lockCurrentVersion sync.RWMutex + lockIsBinary sync.RWMutex + lockIsLocal sync.RWMutex + lockIsPinned sync.RWMutex + lockName sync.RWMutex + lockPath sync.RWMutex + lockTemplatePath sync.RWMutex + lockURL sync.RWMutex + lockUpdateAvailable sync.RWMutex + lockViewPath sync.RWMutex +} + +// Aliases calls AliasesFunc. +func (mock *ExtensionMock) Aliases() ([]Alias, error) { + if mock.AliasesFunc == nil { + panic("ExtensionMock.AliasesFunc: method is nil but Extension.Aliases was just called") + } + callInfo := struct { + }{} + mock.lockAliases.Lock() + mock.calls.Aliases = append(mock.calls.Aliases, callInfo) + mock.lockAliases.Unlock() + return mock.AliasesFunc() +} + +// AliasesCalls gets all the calls that were made to Aliases. +// Check the length with: +// +// len(mockedExtension.AliasesCalls()) +func (mock *ExtensionMock) AliasesCalls() []struct { +} { + var calls []struct { + } + mock.lockAliases.RLock() + calls = mock.calls.Aliases + mock.lockAliases.RUnlock() + return calls +} + +// Commands calls CommandsFunc. +func (mock *ExtensionMock) Commands() ([]Command, error) { + if mock.CommandsFunc == nil { + panic("ExtensionMock.CommandsFunc: method is nil but Extension.Commands was just called") + } + callInfo := struct { + }{} + mock.lockCommands.Lock() + mock.calls.Commands = append(mock.calls.Commands, callInfo) + mock.lockCommands.Unlock() + return mock.CommandsFunc() +} + +// CommandsCalls gets all the calls that were made to Commands. +// Check the length with: +// +// len(mockedExtension.CommandsCalls()) +func (mock *ExtensionMock) CommandsCalls() []struct { +} { + var calls []struct { + } + mock.lockCommands.RLock() + calls = mock.calls.Commands + mock.lockCommands.RUnlock() + return calls +} + +// CurrentVersion calls CurrentVersionFunc. +func (mock *ExtensionMock) CurrentVersion() string { + if mock.CurrentVersionFunc == nil { + panic("ExtensionMock.CurrentVersionFunc: method is nil but Extension.CurrentVersion was just called") + } + callInfo := struct { + }{} + mock.lockCurrentVersion.Lock() + mock.calls.CurrentVersion = append(mock.calls.CurrentVersion, callInfo) + mock.lockCurrentVersion.Unlock() + return mock.CurrentVersionFunc() +} + +// CurrentVersionCalls gets all the calls that were made to CurrentVersion. +// Check the length with: +// +// len(mockedExtension.CurrentVersionCalls()) +func (mock *ExtensionMock) CurrentVersionCalls() []struct { +} { + var calls []struct { + } + mock.lockCurrentVersion.RLock() + calls = mock.calls.CurrentVersion + mock.lockCurrentVersion.RUnlock() + return calls +} + +// IsBinary calls IsBinaryFunc. +func (mock *ExtensionMock) IsBinary() bool { + if mock.IsBinaryFunc == nil { + panic("ExtensionMock.IsBinaryFunc: method is nil but Extension.IsBinary was just called") + } + callInfo := struct { + }{} + mock.lockIsBinary.Lock() + mock.calls.IsBinary = append(mock.calls.IsBinary, callInfo) + mock.lockIsBinary.Unlock() + return mock.IsBinaryFunc() +} + +// IsBinaryCalls gets all the calls that were made to IsBinary. +// Check the length with: +// +// len(mockedExtension.IsBinaryCalls()) +func (mock *ExtensionMock) IsBinaryCalls() []struct { +} { + var calls []struct { + } + mock.lockIsBinary.RLock() + calls = mock.calls.IsBinary + mock.lockIsBinary.RUnlock() + return calls +} + +// IsLocal calls IsLocalFunc. +func (mock *ExtensionMock) IsLocal() bool { + if mock.IsLocalFunc == nil { + panic("ExtensionMock.IsLocalFunc: method is nil but Extension.IsLocal was just called") + } + callInfo := struct { + }{} + mock.lockIsLocal.Lock() + mock.calls.IsLocal = append(mock.calls.IsLocal, callInfo) + mock.lockIsLocal.Unlock() + return mock.IsLocalFunc() +} + +// IsLocalCalls gets all the calls that were made to IsLocal. +// Check the length with: +// +// len(mockedExtension.IsLocalCalls()) +func (mock *ExtensionMock) IsLocalCalls() []struct { +} { + var calls []struct { + } + mock.lockIsLocal.RLock() + calls = mock.calls.IsLocal + mock.lockIsLocal.RUnlock() + return calls +} + +// IsPinned calls IsPinnedFunc. +func (mock *ExtensionMock) IsPinned() bool { + if mock.IsPinnedFunc == nil { + panic("ExtensionMock.IsPinnedFunc: method is nil but Extension.IsPinned was just called") + } + callInfo := struct { + }{} + mock.lockIsPinned.Lock() + mock.calls.IsPinned = append(mock.calls.IsPinned, callInfo) + mock.lockIsPinned.Unlock() + return mock.IsPinnedFunc() +} + +// IsPinnedCalls gets all the calls that were made to IsPinned. +// Check the length with: +// +// len(mockedExtension.IsPinnedCalls()) +func (mock *ExtensionMock) IsPinnedCalls() []struct { +} { + var calls []struct { + } + mock.lockIsPinned.RLock() + calls = mock.calls.IsPinned + mock.lockIsPinned.RUnlock() + return calls +} + +// Name calls NameFunc. +func (mock *ExtensionMock) Name() string { + if mock.NameFunc == nil { + panic("ExtensionMock.NameFunc: method is nil but Extension.Name was just called") + } + callInfo := struct { + }{} + mock.lockName.Lock() + mock.calls.Name = append(mock.calls.Name, callInfo) + mock.lockName.Unlock() + return mock.NameFunc() +} + +// NameCalls gets all the calls that were made to Name. +// Check the length with: +// +// len(mockedExtension.NameCalls()) +func (mock *ExtensionMock) NameCalls() []struct { +} { + var calls []struct { + } + mock.lockName.RLock() + calls = mock.calls.Name + mock.lockName.RUnlock() + return calls +} + +// Path calls PathFunc. +func (mock *ExtensionMock) Path() string { + if mock.PathFunc == nil { + panic("ExtensionMock.PathFunc: method is nil but Extension.Path was just called") + } + callInfo := struct { + }{} + mock.lockPath.Lock() + mock.calls.Path = append(mock.calls.Path, callInfo) + mock.lockPath.Unlock() + return mock.PathFunc() +} + +// PathCalls gets all the calls that were made to Path. +// Check the length with: +// +// len(mockedExtension.PathCalls()) +func (mock *ExtensionMock) PathCalls() []struct { +} { + var calls []struct { + } + mock.lockPath.RLock() + calls = mock.calls.Path + mock.lockPath.RUnlock() + return calls +} + +// TemplatePath calls TemplatePathFunc. +func (mock *ExtensionMock) TemplatePath() string { + if mock.TemplatePathFunc == nil { + panic("ExtensionMock.TemplatePathFunc: method is nil but Extension.TemplatePath was just called") + } + callInfo := struct { + }{} + mock.lockTemplatePath.Lock() + mock.calls.TemplatePath = append(mock.calls.TemplatePath, callInfo) + mock.lockTemplatePath.Unlock() + return mock.TemplatePathFunc() +} + +// TemplatePathCalls gets all the calls that were made to TemplatePath. +// Check the length with: +// +// len(mockedExtension.TemplatePathCalls()) +func (mock *ExtensionMock) TemplatePathCalls() []struct { +} { + var calls []struct { + } + mock.lockTemplatePath.RLock() + calls = mock.calls.TemplatePath + mock.lockTemplatePath.RUnlock() + return calls +} + +// URL calls URLFunc. +func (mock *ExtensionMock) URL() string { + if mock.URLFunc == nil { + panic("ExtensionMock.URLFunc: method is nil but Extension.URL was just called") + } + callInfo := struct { + }{} + mock.lockURL.Lock() + mock.calls.URL = append(mock.calls.URL, callInfo) + mock.lockURL.Unlock() + return mock.URLFunc() +} + +// URLCalls gets all the calls that were made to URL. +// Check the length with: +// +// len(mockedExtension.URLCalls()) +func (mock *ExtensionMock) URLCalls() []struct { +} { + var calls []struct { + } + mock.lockURL.RLock() + calls = mock.calls.URL + mock.lockURL.RUnlock() + return calls +} + +// UpdateAvailable calls UpdateAvailableFunc. +func (mock *ExtensionMock) UpdateAvailable() bool { + if mock.UpdateAvailableFunc == nil { + panic("ExtensionMock.UpdateAvailableFunc: method is nil but Extension.UpdateAvailable was just called") + } + callInfo := struct { + }{} + mock.lockUpdateAvailable.Lock() + mock.calls.UpdateAvailable = append(mock.calls.UpdateAvailable, callInfo) + mock.lockUpdateAvailable.Unlock() + return mock.UpdateAvailableFunc() +} + +// UpdateAvailableCalls gets all the calls that were made to UpdateAvailable. +// Check the length with: +// +// len(mockedExtension.UpdateAvailableCalls()) +func (mock *ExtensionMock) UpdateAvailableCalls() []struct { +} { + var calls []struct { + } + mock.lockUpdateAvailable.RLock() + calls = mock.calls.UpdateAvailable + mock.lockUpdateAvailable.RUnlock() + return calls +} + +// ViewPath calls ViewPathFunc. +func (mock *ExtensionMock) ViewPath() string { + if mock.ViewPathFunc == nil { + panic("ExtensionMock.ViewPathFunc: method is nil but Extension.ViewPath was just called") + } + callInfo := struct { + }{} + mock.lockViewPath.Lock() + mock.calls.ViewPath = append(mock.calls.ViewPath, callInfo) + mock.lockViewPath.Unlock() + return mock.ViewPathFunc() +} + +// ViewPathCalls gets all the calls that were made to ViewPath. +// Check the length with: +// +// len(mockedExtension.ViewPathCalls()) +func (mock *ExtensionMock) ViewPathCalls() []struct { +} { + var calls []struct { + } + mock.lockViewPath.RLock() + calls = mock.calls.ViewPath + mock.lockViewPath.RUnlock() + return calls +} diff --git a/pkg/extensions/manager_mock.go b/pkg/extensions/manager_mock.go new file mode 100644 index 000000000..a04123a1c --- /dev/null +++ b/pkg/extensions/manager_mock.go @@ -0,0 +1,492 @@ +// Code generated by moq; DO NOT EDIT. +// github.com/matryer/moq + +package extensions + +import ( + "github.com/reubenmiller/go-c8y-cli/v2/internal/ghrepo" + "io" + "sync" +) + +// Ensure, that ExtensionManagerMock does implement ExtensionManager. +// If this is not the case, regenerate this file with moq. +var _ ExtensionManager = &ExtensionManagerMock{} + +// ExtensionManagerMock is a mock implementation of ExtensionManager. +// +// func TestSomethingThatUsesExtensionManager(t *testing.T) { +// +// // make and configure a mocked ExtensionManager +// mockedExtensionManager := &ExtensionManagerMock{ +// CreateFunc: func(name string, tmplType ExtTemplateType) error { +// panic("mock out the Create method") +// }, +// DispatchFunc: func(args []string, stdin io.Reader, stdout io.Writer, stderr io.Writer) (bool, error) { +// panic("mock out the Dispatch method") +// }, +// EnableDryRunModeFunc: func() { +// panic("mock out the EnableDryRunMode method") +// }, +// ExecuteFunc: func(exe string, args []string, isBinary bool, stdin io.Reader, stdout io.Writer, stderr io.Writer) (bool, error) { +// panic("mock out the Execute method") +// }, +// InstallFunc: func(interfaceMoqParam ghrepo.Interface, s1 string, s2 string) error { +// panic("mock out the Install method") +// }, +// InstallLocalFunc: func(dir string, name string) error { +// panic("mock out the InstallLocal method") +// }, +// ListFunc: func() []Extension { +// panic("mock out the List method") +// }, +// RemoveFunc: func(name string) error { +// panic("mock out the Remove method") +// }, +// UpgradeFunc: func(name string, force bool) error { +// panic("mock out the Upgrade method") +// }, +// } +// +// // use mockedExtensionManager in code that requires ExtensionManager +// // and then make assertions. +// +// } +type ExtensionManagerMock struct { + // CreateFunc mocks the Create method. + CreateFunc func(name string, tmplType ExtTemplateType) error + + // DispatchFunc mocks the Dispatch method. + DispatchFunc func(args []string, stdin io.Reader, stdout io.Writer, stderr io.Writer) (bool, error) + + // EnableDryRunModeFunc mocks the EnableDryRunMode method. + EnableDryRunModeFunc func() + + // ExecuteFunc mocks the Execute method. + ExecuteFunc func(exe string, args []string, isBinary bool, stdin io.Reader, stdout io.Writer, stderr io.Writer) (bool, error) + + // InstallFunc mocks the Install method. + InstallFunc func(interfaceMoqParam ghrepo.Interface, s1 string, s2 string) error + + // InstallLocalFunc mocks the InstallLocal method. + InstallLocalFunc func(dir string, name string) error + + // ListFunc mocks the List method. + ListFunc func() []Extension + + // RemoveFunc mocks the Remove method. + RemoveFunc func(name string) error + + // UpgradeFunc mocks the Upgrade method. + UpgradeFunc func(name string, force bool) error + + // calls tracks calls to the methods. + calls struct { + // Create holds details about calls to the Create method. + Create []struct { + // Name is the name argument value. + Name string + // TmplType is the tmplType argument value. + TmplType ExtTemplateType + } + // Dispatch holds details about calls to the Dispatch method. + Dispatch []struct { + // Args is the args argument value. + Args []string + // Stdin is the stdin argument value. + Stdin io.Reader + // Stdout is the stdout argument value. + Stdout io.Writer + // Stderr is the stderr argument value. + Stderr io.Writer + } + // EnableDryRunMode holds details about calls to the EnableDryRunMode method. + EnableDryRunMode []struct { + } + // Execute holds details about calls to the Execute method. + Execute []struct { + // Exe is the exe argument value. + Exe string + // Args is the args argument value. + Args []string + // IsBinary is the isBinary argument value. + IsBinary bool + // Stdin is the stdin argument value. + Stdin io.Reader + // Stdout is the stdout argument value. + Stdout io.Writer + // Stderr is the stderr argument value. + Stderr io.Writer + } + // Install holds details about calls to the Install method. + Install []struct { + // InterfaceMoqParam is the interfaceMoqParam argument value. + InterfaceMoqParam ghrepo.Interface + // S1 is the s1 argument value. + S1 string + // S2 is the s2 argument value. + S2 string + } + // InstallLocal holds details about calls to the InstallLocal method. + InstallLocal []struct { + // Dir is the dir argument value. + Dir string + // Name is the name argument value. + Name string + } + // List holds details about calls to the List method. + List []struct { + } + // Remove holds details about calls to the Remove method. + Remove []struct { + // Name is the name argument value. + Name string + } + // Upgrade holds details about calls to the Upgrade method. + Upgrade []struct { + // Name is the name argument value. + Name string + // Force is the force argument value. + Force bool + } + } + lockCreate sync.RWMutex + lockDispatch sync.RWMutex + lockEnableDryRunMode sync.RWMutex + lockExecute sync.RWMutex + lockInstall sync.RWMutex + lockInstallLocal sync.RWMutex + lockList sync.RWMutex + lockRemove sync.RWMutex + lockUpgrade sync.RWMutex +} + +// Create calls CreateFunc. +func (mock *ExtensionManagerMock) Create(name string, tmplType ExtTemplateType) error { + if mock.CreateFunc == nil { + panic("ExtensionManagerMock.CreateFunc: method is nil but ExtensionManager.Create was just called") + } + callInfo := struct { + Name string + TmplType ExtTemplateType + }{ + Name: name, + TmplType: tmplType, + } + mock.lockCreate.Lock() + mock.calls.Create = append(mock.calls.Create, callInfo) + mock.lockCreate.Unlock() + return mock.CreateFunc(name, tmplType) +} + +// CreateCalls gets all the calls that were made to Create. +// Check the length with: +// +// len(mockedExtensionManager.CreateCalls()) +func (mock *ExtensionManagerMock) CreateCalls() []struct { + Name string + TmplType ExtTemplateType +} { + var calls []struct { + Name string + TmplType ExtTemplateType + } + mock.lockCreate.RLock() + calls = mock.calls.Create + mock.lockCreate.RUnlock() + return calls +} + +// Dispatch calls DispatchFunc. +func (mock *ExtensionManagerMock) Dispatch(args []string, stdin io.Reader, stdout io.Writer, stderr io.Writer) (bool, error) { + if mock.DispatchFunc == nil { + panic("ExtensionManagerMock.DispatchFunc: method is nil but ExtensionManager.Dispatch was just called") + } + callInfo := struct { + Args []string + Stdin io.Reader + Stdout io.Writer + Stderr io.Writer + }{ + Args: args, + Stdin: stdin, + Stdout: stdout, + Stderr: stderr, + } + mock.lockDispatch.Lock() + mock.calls.Dispatch = append(mock.calls.Dispatch, callInfo) + mock.lockDispatch.Unlock() + return mock.DispatchFunc(args, stdin, stdout, stderr) +} + +// DispatchCalls gets all the calls that were made to Dispatch. +// Check the length with: +// +// len(mockedExtensionManager.DispatchCalls()) +func (mock *ExtensionManagerMock) DispatchCalls() []struct { + Args []string + Stdin io.Reader + Stdout io.Writer + Stderr io.Writer +} { + var calls []struct { + Args []string + Stdin io.Reader + Stdout io.Writer + Stderr io.Writer + } + mock.lockDispatch.RLock() + calls = mock.calls.Dispatch + mock.lockDispatch.RUnlock() + return calls +} + +// EnableDryRunMode calls EnableDryRunModeFunc. +func (mock *ExtensionManagerMock) EnableDryRunMode() { + if mock.EnableDryRunModeFunc == nil { + panic("ExtensionManagerMock.EnableDryRunModeFunc: method is nil but ExtensionManager.EnableDryRunMode was just called") + } + callInfo := struct { + }{} + mock.lockEnableDryRunMode.Lock() + mock.calls.EnableDryRunMode = append(mock.calls.EnableDryRunMode, callInfo) + mock.lockEnableDryRunMode.Unlock() + mock.EnableDryRunModeFunc() +} + +// EnableDryRunModeCalls gets all the calls that were made to EnableDryRunMode. +// Check the length with: +// +// len(mockedExtensionManager.EnableDryRunModeCalls()) +func (mock *ExtensionManagerMock) EnableDryRunModeCalls() []struct { +} { + var calls []struct { + } + mock.lockEnableDryRunMode.RLock() + calls = mock.calls.EnableDryRunMode + mock.lockEnableDryRunMode.RUnlock() + return calls +} + +// Execute calls ExecuteFunc. +func (mock *ExtensionManagerMock) Execute(exe string, args []string, isBinary bool, stdin io.Reader, stdout io.Writer, stderr io.Writer) (bool, error) { + if mock.ExecuteFunc == nil { + panic("ExtensionManagerMock.ExecuteFunc: method is nil but ExtensionManager.Execute was just called") + } + callInfo := struct { + Exe string + Args []string + IsBinary bool + Stdin io.Reader + Stdout io.Writer + Stderr io.Writer + }{ + Exe: exe, + Args: args, + IsBinary: isBinary, + Stdin: stdin, + Stdout: stdout, + Stderr: stderr, + } + mock.lockExecute.Lock() + mock.calls.Execute = append(mock.calls.Execute, callInfo) + mock.lockExecute.Unlock() + return mock.ExecuteFunc(exe, args, isBinary, stdin, stdout, stderr) +} + +// ExecuteCalls gets all the calls that were made to Execute. +// Check the length with: +// +// len(mockedExtensionManager.ExecuteCalls()) +func (mock *ExtensionManagerMock) ExecuteCalls() []struct { + Exe string + Args []string + IsBinary bool + Stdin io.Reader + Stdout io.Writer + Stderr io.Writer +} { + var calls []struct { + Exe string + Args []string + IsBinary bool + Stdin io.Reader + Stdout io.Writer + Stderr io.Writer + } + mock.lockExecute.RLock() + calls = mock.calls.Execute + mock.lockExecute.RUnlock() + return calls +} + +// Install calls InstallFunc. +func (mock *ExtensionManagerMock) Install(interfaceMoqParam ghrepo.Interface, s1 string, s2 string) error { + if mock.InstallFunc == nil { + panic("ExtensionManagerMock.InstallFunc: method is nil but ExtensionManager.Install was just called") + } + callInfo := struct { + InterfaceMoqParam ghrepo.Interface + S1 string + S2 string + }{ + InterfaceMoqParam: interfaceMoqParam, + S1: s1, + S2: s2, + } + mock.lockInstall.Lock() + mock.calls.Install = append(mock.calls.Install, callInfo) + mock.lockInstall.Unlock() + return mock.InstallFunc(interfaceMoqParam, s1, s2) +} + +// InstallCalls gets all the calls that were made to Install. +// Check the length with: +// +// len(mockedExtensionManager.InstallCalls()) +func (mock *ExtensionManagerMock) InstallCalls() []struct { + InterfaceMoqParam ghrepo.Interface + S1 string + S2 string +} { + var calls []struct { + InterfaceMoqParam ghrepo.Interface + S1 string + S2 string + } + mock.lockInstall.RLock() + calls = mock.calls.Install + mock.lockInstall.RUnlock() + return calls +} + +// InstallLocal calls InstallLocalFunc. +func (mock *ExtensionManagerMock) InstallLocal(dir string, name string) error { + if mock.InstallLocalFunc == nil { + panic("ExtensionManagerMock.InstallLocalFunc: method is nil but ExtensionManager.InstallLocal was just called") + } + callInfo := struct { + Dir string + Name string + }{ + Dir: dir, + Name: name, + } + mock.lockInstallLocal.Lock() + mock.calls.InstallLocal = append(mock.calls.InstallLocal, callInfo) + mock.lockInstallLocal.Unlock() + return mock.InstallLocalFunc(dir, name) +} + +// InstallLocalCalls gets all the calls that were made to InstallLocal. +// Check the length with: +// +// len(mockedExtensionManager.InstallLocalCalls()) +func (mock *ExtensionManagerMock) InstallLocalCalls() []struct { + Dir string + Name string +} { + var calls []struct { + Dir string + Name string + } + mock.lockInstallLocal.RLock() + calls = mock.calls.InstallLocal + mock.lockInstallLocal.RUnlock() + return calls +} + +// List calls ListFunc. +func (mock *ExtensionManagerMock) List() []Extension { + if mock.ListFunc == nil { + panic("ExtensionManagerMock.ListFunc: method is nil but ExtensionManager.List was just called") + } + callInfo := struct { + }{} + mock.lockList.Lock() + mock.calls.List = append(mock.calls.List, callInfo) + mock.lockList.Unlock() + return mock.ListFunc() +} + +// ListCalls gets all the calls that were made to List. +// Check the length with: +// +// len(mockedExtensionManager.ListCalls()) +func (mock *ExtensionManagerMock) ListCalls() []struct { +} { + var calls []struct { + } + mock.lockList.RLock() + calls = mock.calls.List + mock.lockList.RUnlock() + return calls +} + +// Remove calls RemoveFunc. +func (mock *ExtensionManagerMock) Remove(name string) error { + if mock.RemoveFunc == nil { + panic("ExtensionManagerMock.RemoveFunc: method is nil but ExtensionManager.Remove was just called") + } + callInfo := struct { + Name string + }{ + Name: name, + } + mock.lockRemove.Lock() + mock.calls.Remove = append(mock.calls.Remove, callInfo) + mock.lockRemove.Unlock() + return mock.RemoveFunc(name) +} + +// RemoveCalls gets all the calls that were made to Remove. +// Check the length with: +// +// len(mockedExtensionManager.RemoveCalls()) +func (mock *ExtensionManagerMock) RemoveCalls() []struct { + Name string +} { + var calls []struct { + Name string + } + mock.lockRemove.RLock() + calls = mock.calls.Remove + mock.lockRemove.RUnlock() + return calls +} + +// Upgrade calls UpgradeFunc. +func (mock *ExtensionManagerMock) Upgrade(name string, force bool) error { + if mock.UpgradeFunc == nil { + panic("ExtensionManagerMock.UpgradeFunc: method is nil but ExtensionManager.Upgrade was just called") + } + callInfo := struct { + Name string + Force bool + }{ + Name: name, + Force: force, + } + mock.lockUpgrade.Lock() + mock.calls.Upgrade = append(mock.calls.Upgrade, callInfo) + mock.lockUpgrade.Unlock() + return mock.UpgradeFunc(name, force) +} + +// UpgradeCalls gets all the calls that were made to Upgrade. +// Check the length with: +// +// len(mockedExtensionManager.UpgradeCalls()) +func (mock *ExtensionManagerMock) UpgradeCalls() []struct { + Name string + Force bool +} { + var calls []struct { + Name string + Force bool + } + mock.lockUpgrade.RLock() + calls = mock.calls.Upgrade + mock.lockUpgrade.RUnlock() + return calls +} diff --git a/pkg/extexec/extexec.go b/pkg/extexec/extexec.go new file mode 100644 index 000000000..0d8a2a24c --- /dev/null +++ b/pkg/extexec/extexec.go @@ -0,0 +1,70 @@ +package extexec + +import ( + "context" + "errors" + "fmt" + "os" + "os/exec" + "runtime" + "strings" + "time" + + "github.com/cli/safeexec" +) + +func IsC8YCommand(cmd []string) bool { + return len(cmd) > 0 && cmd[0] == "c8y" +} + +func ExecuteExternalCommand(name string, fixed_args []string, optional_args ...string) ([]byte, error) { + args := []string{} + for _, a := range fixed_args { + if strings.Contains(a, "%") { + args = append(args, fmt.Sprintf(a, name)) + } else { + args = append(args, a) + } + } + if IsC8YCommand(args) { + args = append(args, optional_args...) + } + + exePath := "" + var err error + if len(args) < 1 { + return nil, errors.New("invalid external command") + } + + if args[0] == "c8y" { + if runtime.GOOS == "windows" { + exePath, err = safeexec.LookPath("c8y.exe") + if err != nil { + return nil, err + } + } else { + exePath, err = safeexec.LookPath("c8y") + if err != nil { + return nil, err + } + } + + if ep, err := os.Executable(); err != nil { + exePath = ep + } + } else { + exePath = args[0] + } + + ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second) + defer cancel() + binaryCommand := exec.CommandContext( + ctx, + exePath, + args[1:]..., + ) + + binaryCommand.Env = append(os.Environ(), "C8Y_SETTINGS_DEFAULTS_OUTPUT=completion", "C8Y_SETTINGS_DEFAULTS_PAGESIZE=20") + binaryCommand.Dir = "." + return binaryCommand.Output() +} diff --git a/pkg/findsh/find.go b/pkg/findsh/find.go new file mode 100644 index 000000000..de1caf8c6 --- /dev/null +++ b/pkg/findsh/find.go @@ -0,0 +1,11 @@ +//go:build !windows +// +build !windows + +package findsh + +import "os/exec" + +// Find locates the `sh` interpreter on the system. +func Find() (string, error) { + return exec.LookPath("sh") +} diff --git a/pkg/findsh/find_windows.go b/pkg/findsh/find_windows.go new file mode 100644 index 000000000..b9bf19a19 --- /dev/null +++ b/pkg/findsh/find_windows.go @@ -0,0 +1,35 @@ +package findsh + +import ( + "os" + "path/filepath" + + "github.com/cli/safeexec" +) + +func Find() (string, error) { + shPath, shErr := safeexec.LookPath("sh") + if shErr == nil { + return shPath, nil + } + + gitPath, err := safeexec.LookPath("git") + if err != nil { + return "", shErr + } + gitDir := filepath.Dir(gitPath) + + // regular Git for Windows install + shPath = filepath.Join(gitDir, "..", "bin", "sh.exe") + if _, err := os.Stat(shPath); err == nil { + return filepath.Clean(shPath), nil + } + + // git as a scoop shim + shPath = filepath.Join(gitDir, "..", "apps", "git", "current", "bin", "sh.exe") + if _, err := os.Stat(shPath); err == nil { + return filepath.Clean(shPath), nil + } + + return "", shErr +} diff --git a/pkg/flags/getters.go b/pkg/flags/getters.go index 0ee213fb6..34f2d402a 100644 --- a/pkg/flags/getters.go +++ b/pkg/flags/getters.go @@ -6,6 +6,7 @@ import ( "encoding/pem" "errors" "fmt" + "io" "log" "net/http" "os" @@ -861,6 +862,30 @@ func WithFilePath(opts ...string) GetOption { } } +// WithFileContentsAsString read the file contents and provide the value as a string +func WithFileContentsAsString(opts ...string) GetOption { + return func(cmd *cobra.Command, inputIterators *RequestInputIterators) (string, interface{}, error) { + src, dst, _ := UnpackGetterOptions("%s", opts...) + + value, err := cmd.Flags().GetString(src) + if err != nil { + return dst, value, err + } + file, err := os.Open(value) + + if err != nil { + return "", "", err + } + + contents, err := io.ReadAll(file) + if err != nil { + return "", "", err + } + + return dst, string(contents), err + } +} + // RawString raw string type type RawString string @@ -1054,6 +1079,7 @@ type PipelineOptions struct { Property string `json:"property"` Aliases []string `json:"aliases"` IsID bool `json:"isID"` + Values []string `json:"values"` Validator iterator.Validator `json:"-"` Formatter func([]byte) []byte `json:"-"` Format string `json:"-"` diff --git a/pkg/flags/helpers.go b/pkg/flags/helpers.go index e9a60c5fe..845670b10 100644 --- a/pkg/flags/helpers.go +++ b/pkg/flags/helpers.go @@ -1,7 +1,6 @@ package flags import ( - "io/ioutil" "os" "strings" ) @@ -14,7 +13,7 @@ func resolveContents(content string) string { return content } - fileContent, err := ioutil.ReadFile(content) + fileContent, err := os.ReadFile(content) if err != nil { return content } diff --git a/pkg/flags/pipeline.go b/pkg/flags/pipeline.go index 160fe6191..9a97ba5e2 100644 --- a/pkg/flags/pipeline.go +++ b/pkg/flags/pipeline.go @@ -26,6 +26,10 @@ func (e *ParameterError) Error() string { // or from the pipeline // It will automatically try to get the value from a String or a StringSlice flag func NewFlagWithPipeIterator(cmd *cobra.Command, pipeOpt *PipelineOptions, supportsPipeline bool) (iterator.Iterator, error) { + // Use manual values if provided + if len(pipeOpt.Values) > 0 { + return iterator.NewSliceIterator(pipeOpt.Values, pipeOpt.Format), nil + } if supportsPipeline && !pipeOpt.Disabled { sourceProperties := make([]string, 0) if len(pipeOpt.Aliases) > 0 { @@ -109,7 +113,7 @@ func GetFlagStringValues(cmd *cobra.Command, name string) ([]string, error) { return items, nil } -// ErrInvalidIDFormat invalid ID foratm +// ErrInvalidIDFormat invalid ID format var ErrInvalidIDFormat = errors.New("invalid id format") // ValidateID returns an error if the input value does not match an id diff --git a/pkg/flags/template.go b/pkg/flags/template.go index 81fb7323e..50fe7b3da 100644 --- a/pkg/flags/template.go +++ b/pkg/flags/template.go @@ -2,7 +2,6 @@ package flags import ( "fmt" - "io/ioutil" "os" "strings" @@ -82,7 +81,7 @@ func getContents(content string) string { return content } - fileContent, err := ioutil.ReadFile(content) + fileContent, err := os.ReadFile(content) if err != nil { return content } diff --git a/pkg/git/client.go b/pkg/git/client.go new file mode 100644 index 000000000..4901547a2 --- /dev/null +++ b/pkg/git/client.go @@ -0,0 +1,605 @@ +package git + +import ( + "bytes" + "context" + "errors" + "fmt" + "io" + "net/url" + "os/exec" + "path" + "regexp" + "runtime" + "sort" + "strings" + "sync" + + "github.com/cli/safeexec" +) + +var remoteRE = regexp.MustCompile(`(.+)\s+(.+)\s+\((push|fetch)\)`) + +type Client struct { + GhPath string + RepoDir string + GitPath string + Stderr io.Writer + Stdin io.Reader + Stdout io.Writer + + commandContext commandCtx + mu sync.Mutex +} + +func (c *Client) Command(ctx context.Context, args ...string) (*gitCommand, error) { + if c.RepoDir != "" { + args = append([]string{"-C", c.RepoDir}, args...) + } + commandContext := exec.CommandContext + if c.commandContext != nil { + commandContext = c.commandContext + } + var err error + c.mu.Lock() + if c.GitPath == "" { + c.GitPath, err = resolveGitPath() + } + c.mu.Unlock() + if err != nil { + return nil, err + } + cmd := commandContext(ctx, c.GitPath, args...) + cmd.Stderr = c.Stderr + cmd.Stdin = c.Stdin + cmd.Stdout = c.Stdout + return &gitCommand{cmd}, nil +} + +// AuthenticatedCommand is a wrapper around Command that included configuration to use gh +// as the credential helper for git. +func (c *Client) AuthenticatedCommand(ctx context.Context, args ...string) (*gitCommand, error) { + preArgs := []string{"-c", "credential.helper="} + if c.GhPath == "" { + // Assumes that gh is in PATH. + c.GhPath = "gh" + } + credHelper := fmt.Sprintf("!%q auth git-credential", c.GhPath) + preArgs = append(preArgs, "-c", fmt.Sprintf("credential.helper=%s", credHelper)) + args = append(preArgs, args...) + return c.Command(ctx, args...) +} + +func (c *Client) Remotes(ctx context.Context) (RemoteSet, error) { + remoteArgs := []string{"remote", "-v"} + remoteCmd, err := c.Command(ctx, remoteArgs...) + if err != nil { + return nil, err + } + remoteOut, remoteErr := remoteCmd.Output() + if remoteErr != nil { + return nil, remoteErr + } + + configArgs := []string{"config", "--get-regexp", `^remote\..*\.gh-resolved$`} + configCmd, err := c.Command(ctx, configArgs...) + if err != nil { + return nil, err + } + configOut, configErr := configCmd.Output() + if configErr != nil { + // Ignore exit code 1 as it means there are no resolved remotes. + var gitErr *GitError + if ok := errors.As(configErr, &gitErr); ok && gitErr.ExitCode != 1 { + return nil, gitErr + } + } + + remotes := parseRemotes(outputLines(remoteOut)) + populateResolvedRemotes(remotes, outputLines(configOut)) + sort.Sort(remotes) + return remotes, nil +} + +func (c *Client) UpdateRemoteURL(ctx context.Context, name, url string) error { + args := []string{"remote", "set-url", name, url} + cmd, err := c.Command(ctx, args...) + if err != nil { + return err + } + _, err = cmd.Output() + if err != nil { + return err + } + return nil +} + +func (c *Client) SetRemoteResolution(ctx context.Context, name, resolution string) error { + args := []string{"config", "--add", fmt.Sprintf("remote.%s.gh-resolved", name), resolution} + cmd, err := c.Command(ctx, args...) + if err != nil { + return err + } + _, err = cmd.Output() + if err != nil { + return err + } + return nil +} + +// CurrentBranch reads the checked-out branch for the git repository. +func (c *Client) CurrentBranch(ctx context.Context) (string, error) { + args := []string{"symbolic-ref", "--quiet", "HEAD"} + cmd, err := c.Command(ctx, args...) + if err != nil { + return "", err + } + out, err := cmd.Output() + if err != nil { + var gitErr *GitError + if ok := errors.As(err, &gitErr); ok && len(gitErr.Stderr) == 0 { + gitErr.Stderr = "not on any branch" + return "", gitErr + } + return "", err + } + branch := firstLine(out) + return strings.TrimPrefix(branch, "refs/heads/"), nil +} + +// ShowRefs resolves fully-qualified refs to commit hashes. +func (c *Client) ShowRefs(ctx context.Context, refs []string) ([]Ref, error) { + args := append([]string{"show-ref", "--verify", "--"}, refs...) + cmd, err := c.Command(ctx, args...) + if err != nil { + return nil, err + } + // This functionality relies on parsing output from the git command despite + // an error status being returned from git. + out, err := cmd.Output() + var verified []Ref + for _, line := range outputLines(out) { + parts := strings.SplitN(line, " ", 2) + if len(parts) < 2 { + continue + } + verified = append(verified, Ref{ + Hash: parts[0], + Name: parts[1], + }) + } + return verified, err +} + +func (c *Client) Config(ctx context.Context, name string) (string, error) { + args := []string{"config", name} + cmd, err := c.Command(ctx, args...) + if err != nil { + return "", err + } + out, err := cmd.Output() + if err != nil { + var gitErr *GitError + if ok := errors.As(err, &gitErr); ok && gitErr.ExitCode == 1 { + gitErr.Stderr = fmt.Sprintf("unknown config key %s", name) + return "", gitErr + } + return "", err + } + return firstLine(out), nil +} + +func (c *Client) UncommittedChangeCount(ctx context.Context) (int, error) { + args := []string{"status", "--porcelain"} + cmd, err := c.Command(ctx, args...) + if err != nil { + return 0, err + } + out, err := cmd.Output() + if err != nil { + return 0, err + } + lines := strings.Split(string(out), "\n") + count := 0 + for _, l := range lines { + if l != "" { + count++ + } + } + return count, nil +} + +func (c *Client) Commits(ctx context.Context, baseRef, headRef string) ([]*Commit, error) { + args := []string{"-c", "log.ShowSignature=false", "log", "--pretty=format:%H,%s", "--cherry", fmt.Sprintf("%s...%s", baseRef, headRef)} + cmd, err := c.Command(ctx, args...) + if err != nil { + return nil, err + } + out, err := cmd.Output() + if err != nil { + return nil, err + } + commits := []*Commit{} + sha := 0 + title := 1 + for _, line := range outputLines(out) { + split := strings.SplitN(line, ",", 2) + if len(split) != 2 { + continue + } + commits = append(commits, &Commit{ + Sha: split[sha], + Title: split[title], + }) + } + if len(commits) == 0 { + return nil, fmt.Errorf("could not find any commits between %s and %s", baseRef, headRef) + } + return commits, nil +} + +func (c *Client) LastCommit(ctx context.Context) (*Commit, error) { + output, err := c.lookupCommit(ctx, "HEAD", "%H,%s") + if err != nil { + return nil, err + } + idx := bytes.IndexByte(output, ',') + return &Commit{ + Sha: string(output[0:idx]), + Title: strings.TrimSpace(string(output[idx+1:])), + }, nil +} + +func (c *Client) CommitBody(ctx context.Context, sha string) (string, error) { + output, err := c.lookupCommit(ctx, sha, "%b") + return string(output), err +} + +func (c *Client) lookupCommit(ctx context.Context, sha, format string) ([]byte, error) { + args := []string{"-c", "log.ShowSignature=false", "show", "-s", "--pretty=format:" + format, sha} + cmd, err := c.Command(ctx, args...) + if err != nil { + return nil, err + } + out, err := cmd.Output() + if err != nil { + return nil, err + } + return out, nil +} + +// ReadBranchConfig parses the `branch.BRANCH.(remote|merge)` part of git config. +func (c *Client) ReadBranchConfig(ctx context.Context, branch string) (cfg BranchConfig) { + prefix := regexp.QuoteMeta(fmt.Sprintf("branch.%s.", branch)) + args := []string{"config", "--get-regexp", fmt.Sprintf("^%s(remote|merge)$", prefix)} + cmd, err := c.Command(ctx, args...) + if err != nil { + return + } + out, err := cmd.Output() + if err != nil { + return + } + for _, line := range outputLines(out) { + parts := strings.SplitN(line, " ", 2) + if len(parts) < 2 { + continue + } + keys := strings.Split(parts[0], ".") + switch keys[len(keys)-1] { + case "remote": + if strings.Contains(parts[1], ":") { + u, err := ParseURL(parts[1]) + if err != nil { + continue + } + cfg.RemoteURL = u + } else if !isFilesystemPath(parts[1]) { + cfg.RemoteName = parts[1] + } + case "merge": + cfg.MergeRef = parts[1] + } + } + return +} + +func (c *Client) DeleteLocalBranch(ctx context.Context, branch string) error { + args := []string{"branch", "-D", branch} + cmd, err := c.Command(ctx, args...) + if err != nil { + return err + } + _, err = cmd.Output() + if err != nil { + return err + } + return nil +} + +func (c *Client) HasLocalBranch(ctx context.Context, branch string) bool { + args := []string{"rev-parse", "--verify", "refs/heads/" + branch} + cmd, err := c.Command(ctx, args...) + if err != nil { + return false + } + _, err = cmd.Output() + return err == nil +} + +func (c *Client) CheckoutBranch(ctx context.Context, branch string) error { + args := []string{"checkout", branch} + cmd, err := c.Command(ctx, args...) + if err != nil { + return err + } + _, err = cmd.Output() + if err != nil { + return err + } + return nil +} + +func (c *Client) CheckoutNewBranch(ctx context.Context, remoteName, branch string) error { + track := fmt.Sprintf("%s/%s", remoteName, branch) + args := []string{"checkout", "-b", branch, "--track", track} + cmd, err := c.Command(ctx, args...) + if err != nil { + return err + } + _, err = cmd.Output() + if err != nil { + return err + } + return nil +} + +// ToplevelDir returns the top-level directory path of the current repository. +func (c *Client) ToplevelDir(ctx context.Context) (string, error) { + args := []string{"rev-parse", "--show-toplevel"} + cmd, err := c.Command(ctx, args...) + if err != nil { + return "", err + } + out, err := cmd.Output() + if err != nil { + return "", err + } + return firstLine(out), nil +} + +func (c *Client) GitDir(ctx context.Context) (string, error) { + args := []string{"rev-parse", "--git-dir"} + cmd, err := c.Command(ctx, args...) + if err != nil { + return "", err + } + out, err := cmd.Output() + if err != nil { + return "", err + } + return firstLine(out), nil +} + +// Show current directory relative to the top-level directory of repository. +func (c *Client) PathFromRoot(ctx context.Context) string { + args := []string{"rev-parse", "--show-prefix"} + cmd, err := c.Command(ctx, args...) + if err != nil { + return "" + } + out, err := cmd.Output() + if err != nil { + return "" + } + if path := firstLine(out); path != "" { + return path[:len(path)-1] + } + return "" +} + +// Below are commands that make network calls and need authentication credentials supplied from gh. + +func (c *Client) Fetch(ctx context.Context, remote string, refspec string, mods ...CommandModifier) error { + args := []string{"fetch", remote, refspec} + // TODO: Use AuthenticatedCommand + cmd, err := c.Command(ctx, args...) + if err != nil { + return err + } + for _, mod := range mods { + mod(cmd) + } + return cmd.Run() +} + +func (c *Client) Pull(ctx context.Context, remote, branch string, mods ...CommandModifier) error { + args := []string{"pull", "--ff-only", remote, branch} + // TODO: Use AuthenticatedCommand + cmd, err := c.Command(ctx, args...) + if err != nil { + return err + } + for _, mod := range mods { + mod(cmd) + } + return cmd.Run() +} + +func (c *Client) Push(ctx context.Context, remote string, ref string, mods ...CommandModifier) error { + args := []string{"push", "--set-upstream", remote, ref} + // TODO: Use AuthenticatedCommand + cmd, err := c.Command(ctx, args...) + if err != nil { + return err + } + for _, mod := range mods { + mod(cmd) + } + return cmd.Run() +} + +func (c *Client) Clone(ctx context.Context, cloneURL string, args []string, mods ...CommandModifier) (string, error) { + cloneArgs, target := parseCloneArgs(args) + cloneArgs = append(cloneArgs, cloneURL) + // If the args contain an explicit target, pass it to clone otherwise, + // parse the URL to determine where git cloned it to so we can return it. + if target != "" { + cloneArgs = append(cloneArgs, target) + } else { + target = path.Base(strings.TrimSuffix(cloneURL, ".git")) + } + cloneArgs = append([]string{"clone"}, cloneArgs...) + // TODO: Use AuthenticatedCommand + cmd, err := c.Command(ctx, cloneArgs...) + if err != nil { + return "", err + } + for _, mod := range mods { + mod(cmd) + } + err = cmd.Run() + if err != nil { + return "", err + } + return target, nil +} + +func (c *Client) AddRemote(ctx context.Context, name, urlStr string, trackingBranches []string, mods ...CommandModifier) (*Remote, error) { + args := []string{"remote", "add"} + for _, branch := range trackingBranches { + args = append(args, "-t", branch) + } + args = append(args, "-f", name, urlStr) + // TODO: Use AuthenticatedCommand + cmd, err := c.Command(ctx, args...) + if err != nil { + return nil, err + } + for _, mod := range mods { + mod(cmd) + } + if _, err := cmd.Output(); err != nil { + return nil, err + } + var urlParsed *url.URL + if strings.HasPrefix(urlStr, "https") { + urlParsed, err = url.Parse(urlStr) + if err != nil { + return nil, err + } + } else { + urlParsed, err = ParseURL(urlStr) + if err != nil { + return nil, err + } + } + remote := &Remote{ + Name: name, + FetchURL: urlParsed, + PushURL: urlParsed, + } + return remote, nil +} + +func resolveGitPath() (string, error) { + path, err := safeexec.LookPath("git") + if err != nil { + if errors.Is(err, exec.ErrNotFound) { + programName := "git" + if runtime.GOOS == "windows" { + programName = "Git for Windows" + } + return "", &NotInstalled{ + message: fmt.Sprintf("unable to find git executable in PATH; please install %s before retrying", programName), + err: err, + } + } + return "", err + } + return path, nil +} + +func isFilesystemPath(p string) bool { + return p == "." || strings.HasPrefix(p, "./") || strings.HasPrefix(p, "/") +} + +func outputLines(output []byte) []string { + lines := strings.TrimSuffix(string(output), "\n") + return strings.Split(lines, "\n") +} + +func firstLine(output []byte) string { + if i := bytes.IndexAny(output, "\n"); i >= 0 { + return string(output)[0:i] + } + return string(output) +} + +func parseCloneArgs(extraArgs []string) (args []string, target string) { + args = extraArgs + if len(args) > 0 { + if !strings.HasPrefix(args[0], "-") { + target, args = args[0], args[1:] + } + } + return +} + +func parseRemotes(remotesStr []string) RemoteSet { + remotes := RemoteSet{} + for _, r := range remotesStr { + match := remoteRE.FindStringSubmatch(r) + if match == nil { + continue + } + name := strings.TrimSpace(match[1]) + urlStr := strings.TrimSpace(match[2]) + urlType := strings.TrimSpace(match[3]) + + url, err := ParseURL(urlStr) + if err != nil { + continue + } + + var rem *Remote + if len(remotes) > 0 { + rem = remotes[len(remotes)-1] + if name != rem.Name { + rem = nil + } + } + if rem == nil { + rem = &Remote{Name: name} + remotes = append(remotes, rem) + } + + switch urlType { + case "fetch": + rem.FetchURL = url + case "push": + rem.PushURL = url + } + } + return remotes +} + +func populateResolvedRemotes(remotes RemoteSet, resolved []string) { + for _, l := range resolved { + parts := strings.SplitN(l, " ", 2) + if len(parts) < 2 { + continue + } + rp := strings.SplitN(parts[0], ".", 3) + if len(rp) < 2 { + continue + } + name := rp[1] + for _, r := range remotes { + if r.Name == name { + r.Resolved = parts[1] + break + } + } + } +} diff --git a/pkg/git/client_test.go b/pkg/git/client_test.go new file mode 100644 index 000000000..c8ea57246 --- /dev/null +++ b/pkg/git/client_test.go @@ -0,0 +1,1170 @@ +package git + +import ( + "bytes" + "context" + "errors" + "fmt" + "os" + "os/exec" + "path/filepath" + "strconv" + "strings" + "testing" + + "github.com/stretchr/testify/assert" +) + +func TestClientCommand(t *testing.T) { + tests := []struct { + name string + repoDir string + gitPath string + wantExe string + wantArgs []string + }{ + { + name: "creates command", + gitPath: "path/to/git", + wantExe: "path/to/git", + wantArgs: []string{"path/to/git", "ref-log"}, + }, + { + name: "adds repo directory configuration", + repoDir: "path/to/repo", + gitPath: "path/to/git", + wantExe: "path/to/git", + wantArgs: []string{"path/to/git", "-C", "path/to/repo", "ref-log"}, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + in, out, errOut := &bytes.Buffer{}, &bytes.Buffer{}, &bytes.Buffer{} + client := Client{ + Stdin: in, + Stdout: out, + Stderr: errOut, + RepoDir: tt.repoDir, + GitPath: tt.gitPath, + } + cmd, err := client.Command(context.Background(), "ref-log") + assert.NoError(t, err) + assert.Equal(t, tt.wantExe, cmd.Path) + assert.Equal(t, tt.wantArgs, cmd.Args) + assert.Equal(t, in, cmd.Stdin) + assert.Equal(t, out, cmd.Stdout) + assert.Equal(t, errOut, cmd.Stderr) + }) + } +} + +func TestClientAuthenticatedCommand(t *testing.T) { + tests := []struct { + name string + path string + wantArgs []string + }{ + { + name: "adds credential helper config options", + path: "path/to/gh", + wantArgs: []string{"path/to/git", "-c", "credential.helper=", "-c", "credential.helper=!\"path/to/gh\" auth git-credential", "fetch"}, + }, + { + name: "fallback when GhPath is not set", + wantArgs: []string{"path/to/git", "-c", "credential.helper=", "-c", "credential.helper=!\"gh\" auth git-credential", "fetch"}, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + client := Client{ + GhPath: tt.path, + GitPath: "path/to/git", + } + cmd, err := client.AuthenticatedCommand(context.Background(), "fetch") + assert.NoError(t, err) + assert.Equal(t, tt.wantArgs, cmd.Args) + }) + } +} + +func TestClientRemotes(t *testing.T) { + tempDir := t.TempDir() + initRepo(t, tempDir) + gitDir := filepath.Join(tempDir, ".git") + remoteFile := filepath.Join(gitDir, "config") + remotes := ` +[remote "origin"] + url = git@example.com:monalisa/origin.git +[remote "test"] + url = git://github.com/hubot/test.git + gh-resolved = other +[remote "upstream"] + url = https://github.com/monalisa/upstream.git + gh-resolved = base +[remote "github"] + url = git@github.com:hubot/github.git +` + f, err := os.OpenFile(remoteFile, os.O_APPEND|os.O_CREATE|os.O_WRONLY, 0755) + assert.NoError(t, err) + _, err = f.Write([]byte(remotes)) + assert.NoError(t, err) + err = f.Close() + assert.NoError(t, err) + client := Client{ + RepoDir: tempDir, + } + rs, err := client.Remotes(context.Background()) + assert.NoError(t, err) + assert.Equal(t, 4, len(rs)) + assert.Equal(t, "upstream", rs[0].Name) + assert.Equal(t, "base", rs[0].Resolved) + assert.Equal(t, "github", rs[1].Name) + assert.Equal(t, "", rs[1].Resolved) + assert.Equal(t, "origin", rs[2].Name) + assert.Equal(t, "", rs[2].Resolved) + assert.Equal(t, "test", rs[3].Name) + assert.Equal(t, "other", rs[3].Resolved) +} + +func TestClientRemotes_no_resolved_remote(t *testing.T) { + tempDir := t.TempDir() + initRepo(t, tempDir) + gitDir := filepath.Join(tempDir, ".git") + remoteFile := filepath.Join(gitDir, "config") + remotes := ` +[remote "origin"] + url = git@example.com:monalisa/origin.git +[remote "test"] + url = git://github.com/hubot/test.git +[remote "upstream"] + url = https://github.com/monalisa/upstream.git +[remote "github"] + url = git@github.com:hubot/github.git +` + f, err := os.OpenFile(remoteFile, os.O_APPEND|os.O_CREATE|os.O_WRONLY, 0755) + assert.NoError(t, err) + _, err = f.Write([]byte(remotes)) + assert.NoError(t, err) + err = f.Close() + assert.NoError(t, err) + client := Client{ + RepoDir: tempDir, + } + rs, err := client.Remotes(context.Background()) + assert.NoError(t, err) + assert.Equal(t, 4, len(rs)) + assert.Equal(t, "upstream", rs[0].Name) + assert.Equal(t, "github", rs[1].Name) + assert.Equal(t, "origin", rs[2].Name) + assert.Equal(t, "", rs[2].Resolved) + assert.Equal(t, "test", rs[3].Name) +} + +func TestParseRemotes(t *testing.T) { + remoteList := []string{ + "mona\tgit@github.com:monalisa/myfork.git (fetch)", + "origin\thttps://github.com/monalisa/octo-cat.git (fetch)", + "origin\thttps://github.com/monalisa/octo-cat-push.git (push)", + "upstream\thttps://example.com/nowhere.git (fetch)", + "upstream\thttps://github.com/hubot/tools (push)", + "zardoz\thttps://example.com/zed.git (push)", + "koke\tgit://github.com/koke/grit.git (fetch)", + "koke\tgit://github.com/koke/grit.git (push)", + } + + r := parseRemotes(remoteList) + assert.Equal(t, 5, len(r)) + + assert.Equal(t, "mona", r[0].Name) + assert.Equal(t, "ssh://git@github.com/monalisa/myfork.git", r[0].FetchURL.String()) + assert.Nil(t, r[0].PushURL) + + assert.Equal(t, "origin", r[1].Name) + assert.Equal(t, "/monalisa/octo-cat.git", r[1].FetchURL.Path) + assert.Equal(t, "/monalisa/octo-cat-push.git", r[1].PushURL.Path) + + assert.Equal(t, "upstream", r[2].Name) + assert.Equal(t, "example.com", r[2].FetchURL.Host) + assert.Equal(t, "github.com", r[2].PushURL.Host) + + assert.Equal(t, "zardoz", r[3].Name) + assert.Nil(t, r[3].FetchURL) + assert.Equal(t, "https://example.com/zed.git", r[3].PushURL.String()) + + assert.Equal(t, "koke", r[4].Name) + assert.Equal(t, "/koke/grit.git", r[4].FetchURL.Path) + assert.Equal(t, "/koke/grit.git", r[4].PushURL.Path) +} + +func TestClientUpdateRemoteURL(t *testing.T) { + tests := []struct { + name string + cmdExitStatus int + cmdStdout string + cmdStderr string + wantCmdArgs string + wantErrorMsg string + }{ + { + name: "update remote url", + wantCmdArgs: `path/to/git remote set-url test https://test.com`, + }, + { + name: "git error", + cmdExitStatus: 1, + cmdStderr: "git error message", + wantCmdArgs: `path/to/git remote set-url test https://test.com`, + wantErrorMsg: "failed to run git: git error message", + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + cmd, cmdCtx := createCommandContext(t, tt.cmdExitStatus, tt.cmdStdout, tt.cmdStderr) + client := Client{ + GitPath: "path/to/git", + commandContext: cmdCtx, + } + err := client.UpdateRemoteURL(context.Background(), "test", "https://test.com") + assert.Equal(t, tt.wantCmdArgs, strings.Join(cmd.Args[3:], " ")) + if tt.wantErrorMsg == "" { + assert.NoError(t, err) + } else { + assert.EqualError(t, err, tt.wantErrorMsg) + } + }) + } +} + +func TestClientSetRemoteResolution(t *testing.T) { + tests := []struct { + name string + cmdExitStatus int + cmdStdout string + cmdStderr string + wantCmdArgs string + wantErrorMsg string + }{ + { + name: "set remote resolution", + wantCmdArgs: `path/to/git config --add remote.origin.gh-resolved base`, + }, + { + name: "git error", + cmdExitStatus: 1, + cmdStderr: "git error message", + wantCmdArgs: `path/to/git config --add remote.origin.gh-resolved base`, + wantErrorMsg: "failed to run git: git error message", + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + cmd, cmdCtx := createCommandContext(t, tt.cmdExitStatus, tt.cmdStdout, tt.cmdStderr) + client := Client{ + GitPath: "path/to/git", + commandContext: cmdCtx, + } + err := client.SetRemoteResolution(context.Background(), "origin", "base") + assert.Equal(t, tt.wantCmdArgs, strings.Join(cmd.Args[3:], " ")) + if tt.wantErrorMsg == "" { + assert.NoError(t, err) + } else { + assert.EqualError(t, err, tt.wantErrorMsg) + } + }) + } +} + +func TestClientCurrentBranch(t *testing.T) { + tests := []struct { + name string + cmdExitStatus int + cmdStdout string + cmdStderr string + wantCmdArgs string + wantErrorMsg string + wantBranch string + }{ + { + name: "branch name", + cmdStdout: "branch-name\n", + wantCmdArgs: `path/to/git symbolic-ref --quiet HEAD`, + wantBranch: "branch-name", + }, + { + name: "ref", + cmdStdout: "refs/heads/branch-name\n", + wantCmdArgs: `path/to/git symbolic-ref --quiet HEAD`, + wantBranch: "branch-name", + }, + { + name: "escaped ref", + cmdStdout: "refs/heads/branch\u00A0with\u00A0non\u00A0breaking\u00A0space\n", + wantCmdArgs: `path/to/git symbolic-ref --quiet HEAD`, + wantBranch: "branch\u00A0with\u00A0non\u00A0breaking\u00A0space", + }, + { + name: "detatched head", + cmdExitStatus: 1, + wantCmdArgs: `path/to/git symbolic-ref --quiet HEAD`, + wantErrorMsg: "failed to run git: not on any branch", + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + cmd, cmdCtx := createCommandContext(t, tt.cmdExitStatus, tt.cmdStdout, tt.cmdStderr) + client := Client{ + GitPath: "path/to/git", + commandContext: cmdCtx, + } + branch, err := client.CurrentBranch(context.Background()) + assert.Equal(t, tt.wantCmdArgs, strings.Join(cmd.Args[3:], " ")) + if tt.wantErrorMsg == "" { + assert.NoError(t, err) + } else { + assert.EqualError(t, err, tt.wantErrorMsg) + } + assert.Equal(t, tt.wantBranch, branch) + }) + } +} + +func TestClientShowRefs(t *testing.T) { + tests := []struct { + name string + cmdExitStatus int + cmdStdout string + cmdStderr string + wantCmdArgs string + wantRefs []Ref + wantErrorMsg string + }{ + { + name: "show refs with one vaid ref and one invalid ref", + cmdExitStatus: 128, + cmdStdout: "9ea76237a557015e73446d33268569a114c0649c refs/heads/valid", + cmdStderr: "fatal: 'refs/heads/invalid' - not a valid ref", + wantCmdArgs: `path/to/git show-ref --verify -- refs/heads/valid refs/heads/invalid`, + wantRefs: []Ref{{ + Hash: "9ea76237a557015e73446d33268569a114c0649c", + Name: "refs/heads/valid", + }}, + wantErrorMsg: "failed to run git: fatal: 'refs/heads/invalid' - not a valid ref", + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + cmd, cmdCtx := createCommandContext(t, tt.cmdExitStatus, tt.cmdStdout, tt.cmdStderr) + client := Client{ + GitPath: "path/to/git", + commandContext: cmdCtx, + } + refs, err := client.ShowRefs(context.Background(), []string{"refs/heads/valid", "refs/heads/invalid"}) + assert.Equal(t, tt.wantCmdArgs, strings.Join(cmd.Args[3:], " ")) + assert.EqualError(t, err, tt.wantErrorMsg) + assert.Equal(t, tt.wantRefs, refs) + }) + } +} + +func TestClientConfig(t *testing.T) { + tests := []struct { + name string + cmdExitStatus int + cmdStdout string + cmdStderr string + wantCmdArgs string + wantOut string + wantErrorMsg string + }{ + { + name: "get config key", + cmdStdout: "test", + wantCmdArgs: `path/to/git config credential.helper`, + wantOut: "test", + }, + { + name: "get unknown config key", + cmdExitStatus: 1, + cmdStderr: "git error message", + wantCmdArgs: `path/to/git config credential.helper`, + wantErrorMsg: "failed to run git: unknown config key credential.helper", + }, + { + name: "git error", + cmdExitStatus: 2, + cmdStderr: "git error message", + wantCmdArgs: `path/to/git config credential.helper`, + wantErrorMsg: "failed to run git: git error message", + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + cmd, cmdCtx := createCommandContext(t, tt.cmdExitStatus, tt.cmdStdout, tt.cmdStderr) + client := Client{ + GitPath: "path/to/git", + commandContext: cmdCtx, + } + out, err := client.Config(context.Background(), "credential.helper") + assert.Equal(t, tt.wantCmdArgs, strings.Join(cmd.Args[3:], " ")) + if tt.wantErrorMsg == "" { + assert.NoError(t, err) + } else { + assert.EqualError(t, err, tt.wantErrorMsg) + } + assert.Equal(t, tt.wantOut, out) + }) + } +} + +func TestClientUncommittedChangeCount(t *testing.T) { + tests := []struct { + name string + cmdExitStatus int + cmdStdout string + cmdStderr string + wantCmdArgs string + wantChangeCount int + }{ + { + name: "no changes", + wantCmdArgs: `path/to/git status --porcelain`, + wantChangeCount: 0, + }, + { + name: "one change", + cmdStdout: " M poem.txt", + wantCmdArgs: `path/to/git status --porcelain`, + wantChangeCount: 1, + }, + { + name: "untracked file", + cmdStdout: " M poem.txt\n?? new.txt", + wantCmdArgs: `path/to/git status --porcelain`, + wantChangeCount: 2, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + cmd, cmdCtx := createCommandContext(t, tt.cmdExitStatus, tt.cmdStdout, tt.cmdStderr) + client := Client{ + GitPath: "path/to/git", + commandContext: cmdCtx, + } + ucc, err := client.UncommittedChangeCount(context.Background()) + assert.Equal(t, tt.wantCmdArgs, strings.Join(cmd.Args[3:], " ")) + assert.NoError(t, err) + assert.Equal(t, tt.wantChangeCount, ucc) + }) + } +} + +func TestClientCommits(t *testing.T) { + tests := []struct { + name string + cmdExitStatus int + cmdStdout string + cmdStderr string + wantCmdArgs string + wantCommits []*Commit + wantErrorMsg string + }{ + { + name: "get commits", + cmdStdout: "6a6872b918c601a0e730710ad8473938a7516d30,testing testability test", + wantCmdArgs: `path/to/git -c log.ShowSignature=false log --pretty=format:%H,%s --cherry SHA1...SHA2`, + wantCommits: []*Commit{{ + Sha: "6a6872b918c601a0e730710ad8473938a7516d30", + Title: "testing testability test", + }}, + }, + { + name: "no commits between SHAs", + wantCmdArgs: `path/to/git -c log.ShowSignature=false log --pretty=format:%H,%s --cherry SHA1...SHA2`, + wantErrorMsg: "could not find any commits between SHA1 and SHA2", + }, + { + name: "git error", + cmdExitStatus: 1, + cmdStderr: "git error message", + wantCmdArgs: `path/to/git -c log.ShowSignature=false log --pretty=format:%H,%s --cherry SHA1...SHA2`, + wantErrorMsg: "failed to run git: git error message", + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + cmd, cmdCtx := createCommandContext(t, tt.cmdExitStatus, tt.cmdStdout, tt.cmdStderr) + client := Client{ + GitPath: "path/to/git", + commandContext: cmdCtx, + } + commits, err := client.Commits(context.Background(), "SHA1", "SHA2") + assert.Equal(t, tt.wantCmdArgs, strings.Join(cmd.Args[3:], " ")) + if tt.wantErrorMsg != "" { + assert.EqualError(t, err, tt.wantErrorMsg) + } else { + assert.NoError(t, err) + } + assert.Equal(t, tt.wantCommits, commits) + }) + } +} + +func TestClientLastCommit(t *testing.T) { + client := Client{ + RepoDir: "./fixtures/simple.git", + } + c, err := client.LastCommit(context.Background()) + assert.NoError(t, err) + assert.Equal(t, "6f1a2405cace1633d89a79c74c65f22fe78f9659", c.Sha) + assert.Equal(t, "Second commit", c.Title) +} + +func TestClientCommitBody(t *testing.T) { + client := Client{ + RepoDir: "./fixtures/simple.git", + } + body, err := client.CommitBody(context.Background(), "6f1a2405cace1633d89a79c74c65f22fe78f9659") + assert.NoError(t, err) + assert.Equal(t, "I'm starting to get the hang of things\n", body) +} + +func TestClientReadBranchConfig(t *testing.T) { + tests := []struct { + name string + cmdExitStatus int + cmdStdout string + cmdStderr string + wantCmdArgs string + wantBranchConfig BranchConfig + }{ + { + name: "read branch config", + cmdStdout: "branch.trunk.remote origin\nbranch.trunk.merge refs/heads/trunk", + wantCmdArgs: `path/to/git config --get-regexp ^branch\.trunk\.(remote|merge)$`, + wantBranchConfig: BranchConfig{RemoteName: "origin", MergeRef: "refs/heads/trunk"}, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + cmd, cmdCtx := createCommandContext(t, tt.cmdExitStatus, tt.cmdStdout, tt.cmdStderr) + client := Client{ + GitPath: "path/to/git", + commandContext: cmdCtx, + } + branchConfig := client.ReadBranchConfig(context.Background(), "trunk") + assert.Equal(t, tt.wantCmdArgs, strings.Join(cmd.Args[3:], " ")) + assert.Equal(t, tt.wantBranchConfig, branchConfig) + }) + } +} + +func TestClientDeleteLocalBranch(t *testing.T) { + tests := []struct { + name string + cmdExitStatus int + cmdStdout string + cmdStderr string + wantCmdArgs string + wantErrorMsg string + }{ + { + name: "delete local branch", + wantCmdArgs: `path/to/git branch -D trunk`, + }, + { + name: "git error", + cmdExitStatus: 1, + cmdStderr: "git error message", + wantCmdArgs: `path/to/git branch -D trunk`, + wantErrorMsg: "failed to run git: git error message", + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + cmd, cmdCtx := createCommandContext(t, tt.cmdExitStatus, tt.cmdStdout, tt.cmdStderr) + client := Client{ + GitPath: "path/to/git", + commandContext: cmdCtx, + } + err := client.DeleteLocalBranch(context.Background(), "trunk") + assert.Equal(t, tt.wantCmdArgs, strings.Join(cmd.Args[3:], " ")) + if tt.wantErrorMsg == "" { + assert.NoError(t, err) + } else { + assert.EqualError(t, err, tt.wantErrorMsg) + } + }) + } +} + +func TestClientHasLocalBranch(t *testing.T) { + tests := []struct { + name string + cmdExitStatus int + cmdStdout string + cmdStderr string + wantCmdArgs string + wantOut bool + }{ + { + name: "has local branch", + wantCmdArgs: `path/to/git rev-parse --verify refs/heads/trunk`, + wantOut: true, + }, + { + name: "does not have local branch", + cmdExitStatus: 1, + wantCmdArgs: `path/to/git rev-parse --verify refs/heads/trunk`, + wantOut: false, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + cmd, cmdCtx := createCommandContext(t, tt.cmdExitStatus, tt.cmdStdout, tt.cmdStderr) + client := Client{ + GitPath: "path/to/git", + commandContext: cmdCtx, + } + out := client.HasLocalBranch(context.Background(), "trunk") + assert.Equal(t, tt.wantCmdArgs, strings.Join(cmd.Args[3:], " ")) + assert.Equal(t, out, tt.wantOut) + }) + } +} + +func TestClientCheckoutBranch(t *testing.T) { + tests := []struct { + name string + cmdExitStatus int + cmdStdout string + cmdStderr string + wantCmdArgs string + wantErrorMsg string + }{ + { + name: "checkout branch", + wantCmdArgs: `path/to/git checkout trunk`, + }, + { + name: "git error", + cmdExitStatus: 1, + cmdStderr: "git error message", + wantCmdArgs: `path/to/git checkout trunk`, + wantErrorMsg: "failed to run git: git error message", + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + cmd, cmdCtx := createCommandContext(t, tt.cmdExitStatus, tt.cmdStdout, tt.cmdStderr) + client := Client{ + GitPath: "path/to/git", + commandContext: cmdCtx, + } + err := client.CheckoutBranch(context.Background(), "trunk") + assert.Equal(t, tt.wantCmdArgs, strings.Join(cmd.Args[3:], " ")) + if tt.wantErrorMsg == "" { + assert.NoError(t, err) + } else { + assert.EqualError(t, err, tt.wantErrorMsg) + } + }) + } +} + +func TestClientCheckoutNewBranch(t *testing.T) { + tests := []struct { + name string + cmdExitStatus int + cmdStdout string + cmdStderr string + wantCmdArgs string + wantErrorMsg string + }{ + { + name: "checkout new branch", + wantCmdArgs: `path/to/git checkout -b trunk --track origin/trunk`, + }, + { + name: "git error", + cmdExitStatus: 1, + cmdStderr: "git error message", + wantCmdArgs: `path/to/git checkout -b trunk --track origin/trunk`, + wantErrorMsg: "failed to run git: git error message", + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + cmd, cmdCtx := createCommandContext(t, tt.cmdExitStatus, tt.cmdStdout, tt.cmdStderr) + client := Client{ + GitPath: "path/to/git", + commandContext: cmdCtx, + } + err := client.CheckoutNewBranch(context.Background(), "origin", "trunk") + assert.Equal(t, tt.wantCmdArgs, strings.Join(cmd.Args[3:], " ")) + if tt.wantErrorMsg == "" { + assert.NoError(t, err) + } else { + assert.EqualError(t, err, tt.wantErrorMsg) + } + }) + } +} + +func TestClientToplevelDir(t *testing.T) { + tests := []struct { + name string + cmdExitStatus int + cmdStdout string + cmdStderr string + wantCmdArgs string + wantDir string + wantErrorMsg string + }{ + { + name: "top level dir", + cmdStdout: "/path/to/repo", + wantCmdArgs: `path/to/git rev-parse --show-toplevel`, + wantDir: "/path/to/repo", + }, + { + name: "git error", + cmdExitStatus: 1, + cmdStderr: "git error message", + wantCmdArgs: `path/to/git rev-parse --show-toplevel`, + wantErrorMsg: "failed to run git: git error message", + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + cmd, cmdCtx := createCommandContext(t, tt.cmdExitStatus, tt.cmdStdout, tt.cmdStderr) + client := Client{ + GitPath: "path/to/git", + commandContext: cmdCtx, + } + dir, err := client.ToplevelDir(context.Background()) + assert.Equal(t, tt.wantCmdArgs, strings.Join(cmd.Args[3:], " ")) + if tt.wantErrorMsg == "" { + assert.NoError(t, err) + } else { + assert.EqualError(t, err, tt.wantErrorMsg) + } + assert.Equal(t, tt.wantDir, dir) + }) + } +} + +func TestClientGitDir(t *testing.T) { + tests := []struct { + name string + cmdExitStatus int + cmdStdout string + cmdStderr string + wantCmdArgs string + wantDir string + wantErrorMsg string + }{ + { + name: "git dir", + cmdStdout: "/path/to/repo/.git", + wantCmdArgs: `path/to/git rev-parse --git-dir`, + wantDir: "/path/to/repo/.git", + }, + { + name: "git error", + cmdExitStatus: 1, + cmdStderr: "git error message", + wantCmdArgs: `path/to/git rev-parse --git-dir`, + wantErrorMsg: "failed to run git: git error message", + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + cmd, cmdCtx := createCommandContext(t, tt.cmdExitStatus, tt.cmdStdout, tt.cmdStderr) + client := Client{ + GitPath: "path/to/git", + commandContext: cmdCtx, + } + dir, err := client.GitDir(context.Background()) + assert.Equal(t, tt.wantCmdArgs, strings.Join(cmd.Args[3:], " ")) + if tt.wantErrorMsg == "" { + assert.NoError(t, err) + } else { + assert.EqualError(t, err, tt.wantErrorMsg) + } + assert.Equal(t, tt.wantDir, dir) + }) + } +} + +func TestClientPathFromRoot(t *testing.T) { + tests := []struct { + name string + cmdExitStatus int + cmdStdout string + cmdStderr string + wantCmdArgs string + wantErrorMsg string + wantDir string + }{ + { + name: "current path from root", + cmdStdout: "some/path/", + wantCmdArgs: `path/to/git rev-parse --show-prefix`, + wantDir: "some/path", + }, + { + name: "git error", + cmdExitStatus: 1, + cmdStderr: "git error message", + wantCmdArgs: `path/to/git rev-parse --show-prefix`, + wantDir: "", + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + cmd, cmdCtx := createCommandContext(t, tt.cmdExitStatus, tt.cmdStdout, tt.cmdStderr) + client := Client{ + GitPath: "path/to/git", + commandContext: cmdCtx, + } + dir := client.PathFromRoot(context.Background()) + assert.Equal(t, tt.wantCmdArgs, strings.Join(cmd.Args[3:], " ")) + assert.Equal(t, tt.wantDir, dir) + }) + } +} + +func TestClientFetch(t *testing.T) { + tests := []struct { + name string + mods []CommandModifier + cmdExitStatus int + cmdStdout string + cmdStderr string + wantCmdArgs string + wantErrorMsg string + }{ + { + name: "fetch", + wantCmdArgs: `path/to/git fetch origin trunk`, + }, + { + name: "accepts command modifiers", + mods: []CommandModifier{WithRepoDir("/path/to/repo")}, + wantCmdArgs: `path/to/git -C /path/to/repo fetch origin trunk`, + }, + { + name: "git error", + cmdExitStatus: 1, + cmdStderr: "git error message", + wantCmdArgs: `path/to/git fetch origin trunk`, + wantErrorMsg: "failed to run git: git error message", + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + cmd, cmdCtx := createCommandContext(t, tt.cmdExitStatus, tt.cmdStdout, tt.cmdStderr) + client := Client{ + GitPath: "path/to/git", + commandContext: cmdCtx, + } + err := client.Fetch(context.Background(), "origin", "trunk", tt.mods...) + assert.Equal(t, tt.wantCmdArgs, strings.Join(cmd.Args[3:], " ")) + if tt.wantErrorMsg == "" { + assert.NoError(t, err) + } else { + assert.EqualError(t, err, tt.wantErrorMsg) + } + }) + } +} + +func TestClientPull(t *testing.T) { + tests := []struct { + name string + mods []CommandModifier + cmdExitStatus int + cmdStdout string + cmdStderr string + wantCmdArgs string + wantErrorMsg string + }{ + { + name: "pull", + wantCmdArgs: `path/to/git pull --ff-only origin trunk`, + }, + { + name: "accepts command modifiers", + mods: []CommandModifier{WithRepoDir("/path/to/repo")}, + wantCmdArgs: `path/to/git -C /path/to/repo pull --ff-only origin trunk`, + }, + { + name: "git error", + cmdExitStatus: 1, + cmdStderr: "git error message", + wantCmdArgs: `path/to/git pull --ff-only origin trunk`, + wantErrorMsg: "failed to run git: git error message", + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + cmd, cmdCtx := createCommandContext(t, tt.cmdExitStatus, tt.cmdStdout, tt.cmdStderr) + client := Client{ + GitPath: "path/to/git", + commandContext: cmdCtx, + } + err := client.Pull(context.Background(), "origin", "trunk", tt.mods...) + assert.Equal(t, tt.wantCmdArgs, strings.Join(cmd.Args[3:], " ")) + if tt.wantErrorMsg == "" { + assert.NoError(t, err) + } else { + assert.EqualError(t, err, tt.wantErrorMsg) + } + }) + } +} + +func TestClientPush(t *testing.T) { + tests := []struct { + name string + mods []CommandModifier + cmdExitStatus int + cmdStdout string + cmdStderr string + wantCmdArgs string + wantErrorMsg string + }{ + { + name: "push", + wantCmdArgs: `path/to/git push --set-upstream origin trunk`, + }, + { + name: "accepts command modifiers", + mods: []CommandModifier{WithRepoDir("/path/to/repo")}, + wantCmdArgs: `path/to/git -C /path/to/repo push --set-upstream origin trunk`, + }, + { + name: "git error", + cmdExitStatus: 1, + cmdStderr: "git error message", + wantCmdArgs: `path/to/git push --set-upstream origin trunk`, + wantErrorMsg: "failed to run git: git error message", + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + cmd, cmdCtx := createCommandContext(t, tt.cmdExitStatus, tt.cmdStdout, tt.cmdStderr) + client := Client{ + GitPath: "path/to/git", + commandContext: cmdCtx, + } + err := client.Push(context.Background(), "origin", "trunk", tt.mods...) + assert.Equal(t, tt.wantCmdArgs, strings.Join(cmd.Args[3:], " ")) + if tt.wantErrorMsg == "" { + assert.NoError(t, err) + } else { + assert.EqualError(t, err, tt.wantErrorMsg) + } + }) + } +} + +func TestClientClone(t *testing.T) { + tests := []struct { + name string + mods []CommandModifier + cmdExitStatus int + cmdStdout string + cmdStderr string + wantCmdArgs string + wantTarget string + wantErrorMsg string + }{ + { + name: "clone", + wantCmdArgs: `path/to/git clone github.com/cli/cli`, + wantTarget: "cli", + }, + { + name: "accepts command modifiers", + mods: []CommandModifier{WithRepoDir("/path/to/repo")}, + wantCmdArgs: `path/to/git -C /path/to/repo clone github.com/cli/cli`, + wantTarget: "cli", + }, + { + name: "git error", + cmdExitStatus: 1, + cmdStderr: "git error message", + wantCmdArgs: `path/to/git clone github.com/cli/cli`, + wantErrorMsg: "failed to run git: git error message", + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + cmd, cmdCtx := createCommandContext(t, tt.cmdExitStatus, tt.cmdStdout, tt.cmdStderr) + client := Client{ + GitPath: "path/to/git", + commandContext: cmdCtx, + } + target, err := client.Clone(context.Background(), "github.com/cli/cli", []string{}, tt.mods...) + assert.Equal(t, tt.wantCmdArgs, strings.Join(cmd.Args[3:], " ")) + if tt.wantErrorMsg == "" { + assert.NoError(t, err) + } else { + assert.EqualError(t, err, tt.wantErrorMsg) + } + assert.Equal(t, tt.wantTarget, target) + }) + } +} + +func TestParseCloneArgs(t *testing.T) { + type wanted struct { + args []string + dir string + } + tests := []struct { + name string + args []string + want wanted + }{ + { + name: "args and target", + args: []string{"target_directory", "-o", "upstream", "--depth", "1"}, + want: wanted{ + args: []string{"-o", "upstream", "--depth", "1"}, + dir: "target_directory", + }, + }, + { + name: "only args", + args: []string{"-o", "upstream", "--depth", "1"}, + want: wanted{ + args: []string{"-o", "upstream", "--depth", "1"}, + dir: "", + }, + }, + { + name: "only target", + args: []string{"target_directory"}, + want: wanted{ + args: []string{}, + dir: "target_directory", + }, + }, + { + name: "no args", + args: []string{}, + want: wanted{ + args: []string{}, + dir: "", + }, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + args, dir := parseCloneArgs(tt.args) + got := wanted{args: args, dir: dir} + assert.Equal(t, got, tt.want) + }) + } +} + +func TestClientAddRemote(t *testing.T) { + tests := []struct { + title string + name string + url string + branches []string + dir string + cmdExitStatus int + cmdStdout string + cmdStderr string + wantCmdArgs string + wantErrorMsg string + }{ + { + title: "fetch all", + name: "test", + url: "URL", + dir: "DIRECTORY", + branches: []string{}, + wantCmdArgs: `path/to/git -C DIRECTORY remote add -f test URL`, + }, + { + title: "fetch specific branches only", + name: "test", + url: "URL", + dir: "DIRECTORY", + branches: []string{"trunk", "dev"}, + wantCmdArgs: `path/to/git -C DIRECTORY remote add -t trunk -t dev -f test URL`, + }, + } + for _, tt := range tests { + t.Run(tt.title, func(t *testing.T) { + cmd, cmdCtx := createCommandContext(t, tt.cmdExitStatus, tt.cmdStdout, tt.cmdStderr) + client := Client{ + GitPath: "path/to/git", + RepoDir: tt.dir, + commandContext: cmdCtx, + } + _, err := client.AddRemote(context.Background(), tt.name, tt.url, tt.branches) + assert.Equal(t, tt.wantCmdArgs, strings.Join(cmd.Args[3:], " ")) + assert.NoError(t, err) + }) + } +} + +func initRepo(t *testing.T, dir string) { + errBuf := &bytes.Buffer{} + inBuf := &bytes.Buffer{} + outBuf := &bytes.Buffer{} + client := Client{ + RepoDir: dir, + Stderr: errBuf, + Stdin: inBuf, + Stdout: outBuf, + } + cmd, err := client.Command(context.Background(), []string{"init", "--quiet"}...) + assert.NoError(t, err) + _, err = cmd.Output() + assert.NoError(t, err) +} + +func TestHelperProcess(t *testing.T) { + if os.Getenv("GH_WANT_HELPER_PROCESS") != "1" { + return + } + if err := func(args []string) error { + fmt.Fprint(os.Stdout, os.Getenv("GH_HELPER_PROCESS_STDOUT")) + exitStatus := os.Getenv("GH_HELPER_PROCESS_EXIT_STATUS") + if exitStatus != "0" { + return errors.New("error") + } + return nil + }(os.Args[3:]); err != nil { + fmt.Fprint(os.Stderr, os.Getenv("GH_HELPER_PROCESS_STDERR")) + exitStatus := os.Getenv("GH_HELPER_PROCESS_EXIT_STATUS") + i, err := strconv.Atoi(exitStatus) + if err != nil { + os.Exit(1) + } + os.Exit(i) + } + os.Exit(0) +} + +func createCommandContext(t *testing.T, exitStatus int, stdout, stderr string) (*exec.Cmd, commandCtx) { + cmd := exec.CommandContext(context.Background(), os.Args[0], "-test.run=TestHelperProcess", "--") + cmd.Env = []string{ + "GH_WANT_HELPER_PROCESS=1", + fmt.Sprintf("GH_HELPER_PROCESS_STDOUT=%s", stdout), + fmt.Sprintf("GH_HELPER_PROCESS_STDERR=%s", stderr), + fmt.Sprintf("GH_HELPER_PROCESS_EXIT_STATUS=%v", exitStatus), + } + return cmd, func(ctx context.Context, exe string, args ...string) *exec.Cmd { + cmd.Args = append(cmd.Args, exe) + cmd.Args = append(cmd.Args, args...) + return cmd + } +} diff --git a/pkg/git/command.go b/pkg/git/command.go new file mode 100644 index 000000000..fa5924d82 --- /dev/null +++ b/pkg/git/command.go @@ -0,0 +1,97 @@ +package git + +import ( + "bytes" + "context" + "errors" + "io" + "os/exec" + + "github.com/reubenmiller/go-c8y-cli/v2/internal/run" +) + +type commandCtx = func(ctx context.Context, name string, args ...string) *exec.Cmd + +type gitCommand struct { + *exec.Cmd +} + +func (gc *gitCommand) Run() error { + stderr := &bytes.Buffer{} + if gc.Cmd.Stderr == nil { + gc.Cmd.Stderr = stderr + } + + err := run.PrepareCmd(gc.Cmd).Run() + if err != nil { + ge := GitError{err: err, Stderr: stderr.String()} + var exitError *exec.ExitError + if errors.As(err, &exitError) { + ge.ExitCode = exitError.ExitCode() + } + return &ge + } + return nil +} + +func (gc *gitCommand) Output() ([]byte, error) { + gc.Stdout = nil + gc.Stderr = nil + out, err := run.PrepareCmd(gc.Cmd).Output() + if err != nil { + ge := GitError{err: err} + var exitError *exec.ExitError + if errors.As(err, &exitError) { + ge.Stderr = string(exitError.Stderr) + ge.ExitCode = exitError.ExitCode() + } + err = &ge + } + return out, err +} + +func (gc *gitCommand) setRepoDir(repoDir string) { + for i, arg := range gc.Args { + if arg == "-C" { + gc.Args[i+1] = repoDir + return + } + } + // Handle "--" invocations for testing purposes. + var index int + for i, arg := range gc.Args { + if arg == "--" { + index = i + 1 + } + } + gc.Args = append(gc.Args[:index+3], gc.Args[index+1:]...) + gc.Args[index+1] = "-C" + gc.Args[index+2] = repoDir +} + +// Allow individual commands to be modified from the default client options. +type CommandModifier func(*gitCommand) + +func WithStderr(stderr io.Writer) CommandModifier { + return func(gc *gitCommand) { + gc.Stderr = stderr + } +} + +func WithStdout(stdout io.Writer) CommandModifier { + return func(gc *gitCommand) { + gc.Stdout = stdout + } +} + +func WithStdin(stdin io.Reader) CommandModifier { + return func(gc *gitCommand) { + gc.Stdin = stdin + } +} + +func WithRepoDir(repoDir string) CommandModifier { + return func(gc *gitCommand) { + gc.setRepoDir(repoDir) + } +} diff --git a/pkg/git/errors.go b/pkg/git/errors.go new file mode 100644 index 000000000..a3f1645aa --- /dev/null +++ b/pkg/git/errors.go @@ -0,0 +1,39 @@ +package git + +import ( + "errors" + "fmt" +) + +// ErrNotOnAnyBranch indicates that the user is in detached HEAD state. +var ErrNotOnAnyBranch = errors.New("git: not on any branch") + +type NotInstalled struct { + message string + err error +} + +func (e *NotInstalled) Error() string { + return e.message +} + +func (e *NotInstalled) Unwrap() error { + return e.err +} + +type GitError struct { + ExitCode int + Stderr string + err error +} + +func (ge *GitError) Error() string { + if ge.Stderr == "" { + return fmt.Sprintf("failed to run git: %v", ge.err) + } + return fmt.Sprintf("failed to run git: %s", ge.Stderr) +} + +func (ge *GitError) Unwrap() error { + return ge.err +} diff --git a/pkg/git/fixtures/.gitignore b/pkg/git/fixtures/.gitignore new file mode 100644 index 000000000..abae30d02 --- /dev/null +++ b/pkg/git/fixtures/.gitignore @@ -0,0 +1 @@ +*.git/COMMIT_EDITMSG diff --git a/pkg/git/fixtures/simple.git/HEAD b/pkg/git/fixtures/simple.git/HEAD new file mode 100644 index 000000000..b870d8262 --- /dev/null +++ b/pkg/git/fixtures/simple.git/HEAD @@ -0,0 +1 @@ +ref: refs/heads/main diff --git a/pkg/git/fixtures/simple.git/config b/pkg/git/fixtures/simple.git/config new file mode 100644 index 000000000..f0858dd73 --- /dev/null +++ b/pkg/git/fixtures/simple.git/config @@ -0,0 +1,9 @@ +[core] + repositoryformatversion = 0 + filemode = true + ;bare = true + ignorecase = true + precomposeunicode = true +[user] + name = Mona the Cat + email = monalisa@github.com diff --git a/pkg/git/fixtures/simple.git/index b/pkg/git/fixtures/simple.git/index new file mode 100644 index 000000000..65d675154 Binary files /dev/null and b/pkg/git/fixtures/simple.git/index differ diff --git a/pkg/git/fixtures/simple.git/logs/HEAD b/pkg/git/fixtures/simple.git/logs/HEAD new file mode 100644 index 000000000..216887f9e --- /dev/null +++ b/pkg/git/fixtures/simple.git/logs/HEAD @@ -0,0 +1,2 @@ +0000000000000000000000000000000000000000 d1e0abfb7d158ed544a202a6958c62d4fc22e12f Mona the Cat 1614174263 +0100 commit (initial): Initial commit +d1e0abfb7d158ed544a202a6958c62d4fc22e12f 6f1a2405cace1633d89a79c74c65f22fe78f9659 Mona the Cat 1614174275 +0100 commit: Second commit diff --git a/pkg/git/fixtures/simple.git/logs/refs/heads/main b/pkg/git/fixtures/simple.git/logs/refs/heads/main new file mode 100644 index 000000000..216887f9e --- /dev/null +++ b/pkg/git/fixtures/simple.git/logs/refs/heads/main @@ -0,0 +1,2 @@ +0000000000000000000000000000000000000000 d1e0abfb7d158ed544a202a6958c62d4fc22e12f Mona the Cat 1614174263 +0100 commit (initial): Initial commit +d1e0abfb7d158ed544a202a6958c62d4fc22e12f 6f1a2405cace1633d89a79c74c65f22fe78f9659 Mona the Cat 1614174275 +0100 commit: Second commit diff --git a/pkg/git/fixtures/simple.git/objects/4b/825dc642cb6eb9a060e54bf8d69288fbee4904 b/pkg/git/fixtures/simple.git/objects/4b/825dc642cb6eb9a060e54bf8d69288fbee4904 new file mode 100644 index 000000000..adf64119a Binary files /dev/null and b/pkg/git/fixtures/simple.git/objects/4b/825dc642cb6eb9a060e54bf8d69288fbee4904 differ diff --git a/pkg/git/fixtures/simple.git/objects/6f/1a2405cace1633d89a79c74c65f22fe78f9659 b/pkg/git/fixtures/simple.git/objects/6f/1a2405cace1633d89a79c74c65f22fe78f9659 new file mode 100644 index 000000000..8f2fded03 Binary files /dev/null and b/pkg/git/fixtures/simple.git/objects/6f/1a2405cace1633d89a79c74c65f22fe78f9659 differ diff --git a/pkg/git/fixtures/simple.git/objects/d1/e0abfb7d158ed544a202a6958c62d4fc22e12f b/pkg/git/fixtures/simple.git/objects/d1/e0abfb7d158ed544a202a6958c62d4fc22e12f new file mode 100644 index 000000000..ec3ada617 --- /dev/null +++ b/pkg/git/fixtures/simple.git/objects/d1/e0abfb7d158ed544a202a6958c62d4fc22e12f @@ -0,0 +1,3 @@ +xNK +Â0tS¼½ ILc +"‚+î¼@’¾˜Mí뢷7"^@˜Í ó‹µbÐZîxFœî†hŽÁbè½´;’l¯KÑôÒ¿r®3<êä3ÂÍ3œKc#-þú"Îk8ÄZ. ¬2êd´=Â^*)ESÛ&ãiqŸˆÉð­â™i†Ï‹P‡ j‚²A¢yáŸé 3*H/ \ No newline at end of file diff --git a/pkg/git/fixtures/simple.git/refs/heads/main b/pkg/git/fixtures/simple.git/refs/heads/main new file mode 100644 index 000000000..8316cdaf5 --- /dev/null +++ b/pkg/git/fixtures/simple.git/refs/heads/main @@ -0,0 +1 @@ +6f1a2405cace1633d89a79c74c65f22fe78f9659 diff --git a/pkg/git/git.go b/pkg/git/git.go new file mode 100644 index 000000000..3a018a11c --- /dev/null +++ b/pkg/git/git.go @@ -0,0 +1,154 @@ +package git + +import ( + "context" + "io" + "os" +) + +func GitCommand(args ...string) (*gitCommand, error) { + c := &Client{} + return c.Command(context.Background(), args...) +} + +func ShowRefs(ref ...string) ([]Ref, error) { + c := &Client{} + return c.ShowRefs(context.Background(), ref) +} + +func CurrentBranch() (string, error) { + c := &Client{} + return c.CurrentBranch(context.Background()) +} + +func Config(name string) (string, error) { + c := &Client{} + return c.Config(context.Background(), name) +} + +func UncommittedChangeCount() (int, error) { + c := &Client{} + return c.UncommittedChangeCount(context.Background()) +} + +func Commits(baseRef, headRef string) ([]*Commit, error) { + c := &Client{} + return c.Commits(context.Background(), baseRef, headRef) +} + +func LastCommit() (*Commit, error) { + c := &Client{} + return c.LastCommit(context.Background()) +} + +func CommitBody(sha string) (string, error) { + c := &Client{} + return c.CommitBody(context.Background(), sha) +} + +func Push(remote string, ref string, cmdIn io.ReadCloser, cmdOut, cmdErr io.Writer) error { + //TODO: Replace with factory GitClient and use AuthenticatedCommand + c := &Client{ + Stdin: cmdIn, + Stdout: cmdOut, + Stderr: cmdErr, + } + return c.Push(context.Background(), remote, ref) +} + +func ReadBranchConfig(branch string) (cfg BranchConfig) { + c := &Client{} + return c.ReadBranchConfig(context.Background(), branch) +} + +func DeleteLocalBranch(branch string) error { + c := &Client{} + return c.DeleteLocalBranch(context.Background(), branch) +} + +func HasLocalBranch(branch string) bool { + c := &Client{} + return c.HasLocalBranch(context.Background(), branch) +} + +func CheckoutBranch(branch string) error { + c := &Client{} + return c.CheckoutBranch(context.Background(), branch) +} + +func CheckoutNewBranch(remoteName, branch string) error { + c := &Client{} + return c.CheckoutNewBranch(context.Background(), remoteName, branch) +} + +func Pull(remote, branch string) error { + //TODO: Replace with factory GitClient and use AuthenticatedCommand + c := &Client{ + Stdin: os.Stdin, + Stdout: os.Stdout, + Stderr: os.Stderr, + } + return c.Pull(context.Background(), remote, branch) +} + +func RunClone(cloneURL string, args []string) (target string, err error) { + //TODO: Replace with factory GitClient and use AuthenticatedCommand + c := &Client{ + Stdin: os.Stdin, + Stdout: os.Stdout, + Stderr: os.Stderr, + } + return c.Clone(context.Background(), cloneURL, args) +} + +func ToplevelDir() (string, error) { + c := &Client{} + return c.ToplevelDir(context.Background()) +} + +func GetDirFromPath(repoDir string) (string, error) { + c := &Client{ + RepoDir: repoDir, + } + return c.GitDir(context.Background()) +} + +func PathFromRepoRoot() string { + c := &Client{} + return c.PathFromRoot(context.Background()) +} + +func Remotes() (RemoteSet, error) { + c := &Client{} + return c.Remotes(context.Background()) +} + +func RemotesForPath(repoDir string) (RemoteSet, error) { + c := &Client{ + RepoDir: repoDir, + } + return c.Remotes(context.Background()) +} + +func AddRemote(name, url string) (*Remote, error) { + c := &Client{} + return c.AddRemote(context.Background(), name, url, []string{}) +} + +func AddNamedRemote(url, name, repoDir string, branches []string) error { + c := &Client{ + RepoDir: repoDir, + } + _, err := c.AddRemote(context.Background(), name, url, branches) + return err +} + +func UpdateRemoteURL(name, url string) error { + c := &Client{} + return c.UpdateRemoteURL(context.Background(), name, url) +} + +func SetRemoteResolution(name, resolution string) error { + c := &Client{} + return c.SetRemoteResolution(context.Background(), name, resolution) +} diff --git a/pkg/git/objects.go b/pkg/git/objects.go new file mode 100644 index 000000000..952b6c335 --- /dev/null +++ b/pkg/git/objects.go @@ -0,0 +1,76 @@ +package git + +import ( + "net/url" + "strings" +) + +// RemoteSet is a slice of git remotes. +type RemoteSet []*Remote + +func (r RemoteSet) Len() int { return len(r) } +func (r RemoteSet) Swap(i, j int) { r[i], r[j] = r[j], r[i] } +func (r RemoteSet) Less(i, j int) bool { + return remoteNameSortScore(r[i].Name) > remoteNameSortScore(r[j].Name) +} + +func remoteNameSortScore(name string) int { + switch strings.ToLower(name) { + case "upstream": + return 3 + case "github": + return 2 + case "origin": + return 1 + default: + return 0 + } +} + +// Remote is a parsed git remote. +type Remote struct { + Name string + Resolved string + FetchURL *url.URL + PushURL *url.URL +} + +func (r *Remote) String() string { + return r.Name +} + +func NewRemote(name string, u string) *Remote { + pu, _ := url.Parse(u) + return &Remote{ + Name: name, + FetchURL: pu, + PushURL: pu, + } +} + +// Ref represents a git commit reference. +type Ref struct { + Hash string + Name string +} + +// TrackingRef represents a ref for a remote tracking branch. +type TrackingRef struct { + RemoteName string + BranchName string +} + +func (r TrackingRef) String() string { + return "refs/remotes/" + r.RemoteName + "/" + r.BranchName +} + +type Commit struct { + Sha string + Title string +} + +type BranchConfig struct { + RemoteName string + RemoteURL *url.URL + MergeRef string +} diff --git a/pkg/git/url.go b/pkg/git/url.go new file mode 100644 index 000000000..1a3e97fd6 --- /dev/null +++ b/pkg/git/url.go @@ -0,0 +1,64 @@ +package git + +import ( + "net/url" + "strings" +) + +func IsURL(u string) bool { + return strings.HasPrefix(u, "git@") || isSupportedProtocol(u) +} + +func isSupportedProtocol(u string) bool { + return strings.HasPrefix(u, "ssh:") || + strings.HasPrefix(u, "git+ssh:") || + strings.HasPrefix(u, "git:") || + strings.HasPrefix(u, "http:") || + strings.HasPrefix(u, "git+https:") || + strings.HasPrefix(u, "https:") +} + +func isPossibleProtocol(u string) bool { + return isSupportedProtocol(u) || + strings.HasPrefix(u, "ftp:") || + strings.HasPrefix(u, "ftps:") || + strings.HasPrefix(u, "file:") +} + +// ParseURL normalizes git remote urls +func ParseURL(rawURL string) (u *url.URL, err error) { + if !isPossibleProtocol(rawURL) && + strings.ContainsRune(rawURL, ':') && + // not a Windows path + !strings.ContainsRune(rawURL, '\\') { + // support scp-like syntax for ssh protocol + rawURL = "ssh://" + strings.Replace(rawURL, ":", "/", 1) + } + + u, err = url.Parse(rawURL) + if err != nil { + return + } + + if u.Scheme == "git+ssh" { + u.Scheme = "ssh" + } + + if u.Scheme == "git+https" { + u.Scheme = "https" + } + + if u.Scheme != "ssh" { + return + } + + if strings.HasPrefix(u.Path, "//") { + u.Path = strings.TrimPrefix(u.Path, "/") + } + + if idx := strings.Index(u.Host, ":"); idx >= 0 { + u.Host = u.Host[0:idx] + } + + return +} diff --git a/pkg/git/url_test.go b/pkg/git/url_test.go new file mode 100644 index 000000000..f5b3b50d0 --- /dev/null +++ b/pkg/git/url_test.go @@ -0,0 +1,220 @@ +package git + +import "testing" + +func TestIsURL(t *testing.T) { + tests := []struct { + name string + url string + want bool + }{ + { + name: "scp-like", + url: "git@example.com:owner/repo", + want: true, + }, + { + name: "scp-like with no user", + url: "example.com:owner/repo", + want: false, + }, + { + name: "ssh", + url: "ssh://git@example.com/owner/repo", + want: true, + }, + { + name: "git", + url: "git://example.com/owner/repo", + want: true, + }, + { + name: "git with extension", + url: "git://example.com/owner/repo.git", + want: true, + }, + { + name: "git+ssh", + url: "git+ssh://git@example.com/owner/repo.git", + want: true, + }, + { + name: "https", + url: "https://example.com/owner/repo.git", + want: true, + }, + { + name: "git+https", + url: "git+https://example.com/owner/repo.git", + want: true, + }, + { + name: "no protocol", + url: "example.com/owner/repo", + want: false, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + if got := IsURL(tt.url); got != tt.want { + t.Errorf("IsURL() = %v, want %v", got, tt.want) + } + }) + } +} + +func TestParseURL(t *testing.T) { + type url struct { + Scheme string + User string + Host string + Path string + } + tests := []struct { + name string + url string + want url + wantErr bool + }{ + { + name: "HTTPS", + url: "https://example.com/owner/repo.git", + want: url{ + Scheme: "https", + User: "", + Host: "example.com", + Path: "/owner/repo.git", + }, + }, + { + name: "HTTP", + url: "http://example.com/owner/repo.git", + want: url{ + Scheme: "http", + User: "", + Host: "example.com", + Path: "/owner/repo.git", + }, + }, + { + name: "git", + url: "git://example.com/owner/repo.git", + want: url{ + Scheme: "git", + User: "", + Host: "example.com", + Path: "/owner/repo.git", + }, + }, + { + name: "ssh", + url: "ssh://git@example.com/owner/repo.git", + want: url{ + Scheme: "ssh", + User: "git", + Host: "example.com", + Path: "/owner/repo.git", + }, + }, + { + name: "ssh with port", + url: "ssh://git@example.com:443/owner/repo.git", + want: url{ + Scheme: "ssh", + User: "git", + Host: "example.com", + Path: "/owner/repo.git", + }, + }, + { + name: "git+ssh", + url: "git+ssh://example.com/owner/repo.git", + want: url{ + Scheme: "ssh", + User: "", + Host: "example.com", + Path: "/owner/repo.git", + }, + }, + { + name: "git+https", + url: "git+https://example.com/owner/repo.git", + want: url{ + Scheme: "https", + User: "", + Host: "example.com", + Path: "/owner/repo.git", + }, + }, + { + name: "scp-like", + url: "git@example.com:owner/repo.git", + want: url{ + Scheme: "ssh", + User: "git", + Host: "example.com", + Path: "/owner/repo.git", + }, + }, + { + name: "scp-like, leading slash", + url: "git@example.com:/owner/repo.git", + want: url{ + Scheme: "ssh", + User: "git", + Host: "example.com", + Path: "/owner/repo.git", + }, + }, + { + name: "file protocol", + url: "file:///example.com/owner/repo.git", + want: url{ + Scheme: "file", + User: "", + Host: "", + Path: "/example.com/owner/repo.git", + }, + }, + { + name: "file path", + url: "/example.com/owner/repo.git", + want: url{ + Scheme: "", + User: "", + Host: "", + Path: "/example.com/owner/repo.git", + }, + }, + { + name: "Windows file path", + url: "C:\\example.com\\owner\\repo.git", + want: url{ + Scheme: "c", + User: "", + Host: "", + Path: "", + }, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + u, err := ParseURL(tt.url) + if (err != nil) != tt.wantErr { + t.Fatalf("got error: %v", err) + } + if u.Scheme != tt.want.Scheme { + t.Errorf("expected scheme %q, got %q", tt.want.Scheme, u.Scheme) + } + if u.User.Username() != tt.want.User { + t.Errorf("expected user %q, got %q", tt.want.User, u.User.Username()) + } + if u.Host != tt.want.Host { + t.Errorf("expected host %q, got %q", tt.want.Host, u.Host) + } + if u.Path != tt.want.Path { + t.Errorf("expected path %q, got %q", tt.want.Path, u.Path) + } + }) + } +} diff --git a/pkg/jsonfilter/jsonfilter.go b/pkg/jsonfilter/jsonfilter.go index bbdb6a18b..f92fddf97 100644 --- a/pkg/jsonfilter/jsonfilter.go +++ b/pkg/jsonfilter/jsonfilter.go @@ -32,12 +32,14 @@ func init() { } type JSONFilters struct { - Logger *logger.Logger - Filters []JSONFilter - Selectors []string - Pluck []string - Flatten bool - AsCSV bool + Logger *logger.Logger + Filters []JSONFilter + Selectors []string + Pluck []string + Flatten bool + AsCSV bool + AsTSV bool + AsCompletionFormat bool } func (f JSONFilters) Apply(jsonValue string, property string, showHeaders bool, setHeaderFunc func(string)) ([]byte, error) { @@ -212,7 +214,7 @@ func filterFlatMap(src map[string]interface{}, dst map[string]interface{}, patte sourceKeys[i] = key i++ } - // Use natural sorting to sory array in an user friendly way + // Use natural sorting to sort array in an user friendly way // i.e. 1, 10, 2 => 1, 2, 10 // Skip sorting for very large key sets if len(sourceKeys) <= 2000000 { @@ -421,7 +423,7 @@ func (f JSONFilters) filterJSON(jsonValue string, property string, showHeaders b for _, myval := range formattedJSON.Array() { if myval.IsObject() { - if line, keys := f.pluckJsonValues(&myval, f.Pluck, f.Flatten, f.AsCSV); line != "" { + if line, keys := f.pluckJsonValues(&myval, f.Pluck); line != "" { outputValues = append(outputValues, line) setHeaderFunc(strings.Join(keys, ",")) } @@ -432,7 +434,7 @@ func (f JSONFilters) filterJSON(jsonValue string, property string, showHeaders b return []byte(strings.Join(outputValues, "\n")), formatErrors(jq.Errors()) } - if line, keys := f.pluckJsonValues(&formattedJSON, f.Pluck, f.Flatten, f.AsCSV); line != "" { + if line, keys := f.pluckJsonValues(&formattedJSON, f.Pluck); line != "" { setHeaderFunc(strings.Join(keys, ",")) return []byte(line), formatErrors(jq.Errors()) } @@ -489,7 +491,7 @@ func resolveKeyName(item *gjson.Result, key string) (name string, value interfac return key, nil, nil } -func (f JSONFilters) pluckJsonValues(item *gjson.Result, properties []string, flat bool, asCSV bool) (string, []string) { +func (f JSONFilters) pluckJsonValues(item *gjson.Result, properties []string) (string, []string) { if item == nil { return "", nil } @@ -514,10 +516,13 @@ func (f JSONFilters) pluckJsonValues(item *gjson.Result, properties []string, fl // json output var v []byte - if flat || asCSV { - if asCSV { - return convertToCSV(flatMap, flatKeys), flatKeys - } + if f.AsCSV { + return convertToCSV(flatMap, flatKeys, ","), flatKeys + } else if f.AsTSV { + return convertToCSV(flatMap, flatKeys, "\t"), flatKeys + } else if f.AsCompletionFormat { + return convertToLine(flatMap, flatKeys), flatKeys + } else if f.Flatten { v, err = json.Marshal(flatMap) } else { // unflatten @@ -560,17 +565,50 @@ func (f JSONFilters) pluckJsonValues(item *gjson.Result, properties []string, fl return output.String(), flatKeys } -func convertToCSV(flatMap map[string]interface{}, keys []string) string { +func convertToCSV(flatMap map[string]interface{}, keys []string, separator string) string { + buf := bytes.Buffer{} + if separator == "" { + separator = "," + } + for i, key := range keys { + if i != 0 { + // handle for empty non-existent values by leaving it blank + buf.WriteString(separator) + } + if value, ok := flatMap[key]; ok { + if marshalledValue, err := json.Marshal(value); err != nil { + Logger.Warningf("failed to marshal value. value=%v, err=%s", value, err) + } else { + if !bytes.Contains(marshalledValue, []byte(",")) { + buf.Write(bytes.Trim(marshalledValue, "\"")) + } else { + buf.Write(marshalledValue) + } + } + } + } + return buf.String() +} + +func convertToLine(flatMap map[string]interface{}, keys []string) string { buf := bytes.Buffer{} for i, key := range keys { if i != 0 { - // handle for empty non-existant values by leaving it blank - buf.WriteByte(',') + // handle for empty non-existent values by leaving it blank + if i == 1 { + buf.WriteString("\t") + } else { + buf.WriteString(" | ") + } } if value, ok := flatMap[key]; ok { if marshalledValue, err := json.Marshal(value); err != nil { Logger.Warningf("failed to marshal value. value=%v, err=%s", value, err) } else { + if i != 0 { + buf.WriteString(key) + buf.WriteString(": ") + } if !bytes.Contains(marshalledValue, []byte(",")) { buf.Write(bytes.Trim(marshalledValue, "\"")) } else { diff --git a/pkg/jsonformatter/jsonformatter.go b/pkg/jsonformatter/jsonformatter.go index 36af45a59..d9cf1fa80 100644 --- a/pkg/jsonformatter/jsonformatter.go +++ b/pkg/jsonformatter/jsonformatter.go @@ -45,10 +45,10 @@ func WithTrimSpace(enabled bool) OutputFormatter { } // WithJSONStreamOutput converts json to json lines so it can be processed in the pipe -func WithJSONStreamOutput(enabled bool, stream bool, asCSV bool) OutputFormatter { +func WithJSONStreamOutput(enabled bool, stream bool, isText bool) OutputFormatter { return func(i io.Writer, b []byte) []byte { return WithOptionalFormatter(enabled, func(input []byte) []byte { - if asCSV { + if isText { if len(input) > 0 { fmt.Fprintf(i, "%s\n", input) } diff --git a/pkg/request/request.go b/pkg/request/request.go index d9b9c8a60..4784c9e74 100644 --- a/pkg/request/request.go +++ b/pkg/request/request.go @@ -787,7 +787,15 @@ func (r *RequestHandler) ProcessResponse(resp *c8y.Response, respError error, co commonOptions.Filters.Pluck = []string{"**"} } case config.ViewsAuto: - props, err := r.DataView.GetView(&inputData, resp.Response.Header.Get("Content-Type")) + viewData := &dataview.ViewData{ + ResponseBody: &inputData, + ContentType: resp.Response.Header.Get("Content-Type"), + Request: resp.Response.Request, + } + if resp.Response != nil { + viewData.Request = resp.Response.Request + } + props, err := r.DataView.GetView(viewData) if err != nil || len(props) == 0 { if err != nil { @@ -859,7 +867,7 @@ func (r *RequestHandler) ProcessResponse(resp *c8y.Response, respError error, co !isJSONResponse, jsonformatter.WithFileOutput(commonOptions.OutputFile != "", commonOptions.OutputFile, false), jsonformatter.WithTrimSpace(true), - jsonformatter.WithJSONStreamOutput(isJSONResponse, consol.IsJSONStream(), consol.IsCSV()), + jsonformatter.WithJSONStreamOutput(isJSONResponse, consol.IsJSONStream(), consol.IsTextOutput()), jsonformatter.WithSuffix(len(responseText) > 0, "\n"), ) } diff --git a/scripts/build-cli/New-C8yApi.ps1 b/scripts/build-cli/New-C8yApi.ps1 index 78d6d99b9..f87fdca06 100644 --- a/scripts/build-cli/New-C8yApi.ps1 +++ b/scripts/build-cli/New-C8yApi.ps1 @@ -27,23 +27,23 @@ $Specification = Get-Content $Path -Raw -Encoding utf8 | ConvertFrom-Json - if ([string]::IsNullOrWhiteSpace($Specification.information.name)) { + if ([string]::IsNullOrWhiteSpace($Specification.group.name)) { Write-Warning "Skipping spec: Specification is missing the information.name property. This is required. file=$Path" continue } - if ($Specification.information.skip -eq $true) { + if ($Specification.group.skip -eq $true) { Write-Warning "Skipping spec: Specification is marked to be skipped using information.skip property. This is required. file=$Path" continue } # Create root command (golang convention is to use lower case packages names) - $packageName = $Specification.information.name.ToLower() + $packageName = $Specification.group.name.ToLower() $CommandOutput = Join-Path $OutputDir -ChildPath $packageName $null = New-Item -Path $CommandOutput -ItemType Directory -Force New-C8yApiGoRootCommand -Specification:$Specification -OutputDir:$CommandOutput Write-Host ("Generating api root command: {0}" -f $CommandOutput) -ForegroundColor Cyan - foreach ($iSpec in $Specification.endpoints) { + foreach ($iSpec in $Specification.commands) { if ($iSpec.skip -eq $true) { Write-Verbose ("Skipping [{0}]" -f $iSpec.name) continue diff --git a/scripts/build-cli/New-C8yApiGoCommand.ps1 b/scripts/build-cli/New-C8yApiGoCommand.ps1 index 22015887c..dc64e0cea 100644 --- a/scripts/build-cli/New-C8yApiGoCommand.ps1 +++ b/scripts/build-cli/New-C8yApiGoCommand.ps1 @@ -120,7 +120,7 @@ $PipelineVariableProperty = if ($iArg.Property) { $iArg.Property } else { $iArg.Name } $PipelineVariableAliases = $iArg.pipelineAliases if (!$PipelineVariableAliases) { - if ($PipelineVariableName -match "device$" -or $iArg.type -match "device$") { + if ($PipelineVariableName -match "device$" -or $iArg.type -eq "device[]") { $PipelineVariableAliases = @( "deviceId", "source.id", @@ -132,7 +132,7 @@ } } - if ($iArg.Type -notmatch "device\b|agent\b|group|devicegroup|self|application|hostedapplication|software\b|softwareName\b|deviceservice\b|softwareversion\b|softwareversionName\b|firmware\b|firmwareName\b|firmwareversion\b|firmwareversionName\b|firmwarepatch\b|firmwarepatchName\b|configuration\b|deviceprofile\b|microservice|\[\]id|\[\]devicerequest") { + if ($iArg.Type -notmatch "device\b|agent\b|group|devicegroup|self|application|hostedapplication|software\b|softwareName\b|deviceservice\b|softwareversion\b|softwareversionName\b|firmware\b|firmwareName\b|firmwareversion\b|firmwareversionName\b|firmwarepatch\b|firmwarepatchName\b|configuration\b|deviceprofile\b|microservice|id\[\]|devicerequest\[\]") { if ($RESTMethod -match "POST" -and $iArg.ArgSource -eq "body") { # Add override capability to piped arguments, so the user can still override piped data with the argument [void] $RESTBodyBuilderOptions.AppendLine("flags.WithOverrideValue(`"$($iarg.Name)`", `"$PipelineVariableProperty`"),") @@ -191,33 +191,32 @@ # Add Completions based on type $ArgType = $iArg.type - switch -Regex ($ArgType) { - "^application|applicationname" { [void] $CompletionBuilderOptions.AppendLine("completion.WithApplication(`"$($iArg.Name)`", func() (*c8y.Client, error) { return ccmd.factory.Client()}),") } + switch ($ArgType) { + { @("application", "applicationname") -contains $_ } { [void] $CompletionBuilderOptions.AppendLine("completion.WithApplication(`"$($iArg.Name)`", func() (*c8y.Client, error) { return ccmd.factory.Client()}),") } "hostedapplication" { [void] $CompletionBuilderOptions.AppendLine("completion.WithHostedApplication(`"$($iArg.Name)`", func() (*c8y.Client, error) { return ccmd.factory.Client()}),") } - "microservice\b" { [void] $CompletionBuilderOptions.AppendLine("completion.WithMicroservice(`"$($iArg.Name)`", func() (*c8y.Client, error) { return ccmd.factory.Client()}),") } - "microservicename\b" { [void] $CompletionBuilderOptions.AppendLine("completion.WithMicroservice(`"$($iArg.Name)`", func() (*c8y.Client, error) { return ccmd.factory.Client()}),") } - "microserviceinstance\b" { [void] $CompletionBuilderOptions.AppendLine("completion.WithMicroserviceInstance(`"$($iArg.Name)`", `"id`", func() (*c8y.Client, error) { return ccmd.factory.Client()}),") } - "role" { [void] $CompletionBuilderOptions.AppendLine("completion.WithUserRole(`"$($iArg.Name)`", func() (*c8y.Client, error) { return ccmd.factory.Client()}),") } - "(\[\])?devicerequest$" { [void] $CompletionBuilderOptions.AppendLine("completion.WithDeviceRegistrationRequest(`"$($iArg.Name)`", func() (*c8y.Client, error) { return ccmd.factory.Client()}),") } - "\[\]user(self)?$" { [void] $CompletionBuilderOptions.AppendLine("completion.WithUser(`"$($iArg.Name)`", func() (*c8y.Client, error) { return ccmd.factory.Client()}),") } - "(\[\])?usergroup$" { [void] $CompletionBuilderOptions.AppendLine("completion.WithUserGroup(`"$($iArg.Name)`", func() (*c8y.Client, error) { return ccmd.factory.Client()}),") } - "(\[\])?devicegroup$" { [void] $CompletionBuilderOptions.AppendLine("completion.WithDeviceGroup(`"$($iArg.Name)`", func() (*c8y.Client, error) { return ccmd.factory.Client()}),") } - "(\[\])?smartgroup$" { [void] $CompletionBuilderOptions.AppendLine("completion.WithSmartGroup(`"$($iArg.Name)`", func() (*c8y.Client, error) { return ccmd.factory.Client()}),") } - "(\[\])?tenant$" { [void] $CompletionBuilderOptions.AppendLine("completion.WithTenantID(`"$($iArg.Name)`", func() (*c8y.Client, error) { return ccmd.factory.Client()}),") } - "(\[\])?tenantname$" { [void] $CompletionBuilderOptions.AppendLine("completion.WithTenantID(`"$($iArg.Name)`", func() (*c8y.Client, error) { return ccmd.factory.Client()}),") } - "(\[\])?device$" { [void] $CompletionBuilderOptions.AppendLine("completion.WithDevice(`"$($iArg.Name)`", func() (*c8y.Client, error) { return ccmd.factory.Client()}),") } - "(\[\])?agent$" { [void] $CompletionBuilderOptions.AppendLine("completion.WithAgent(`"$($iArg.Name)`", func() (*c8y.Client, error) { return ccmd.factory.Client()}),") } - "(\[\])?software(name)?$" { [void] $CompletionBuilderOptions.AppendLine("completion.WithSoftware(`"$($iArg.Name)`", func() (*c8y.Client, error) { return ccmd.factory.Client()}),") } - "(\[\])?softwareversion(name)?$" { [void] $CompletionBuilderOptions.AppendLine("completion.WithSoftwareVersion(`"$($iArg.Name)`", `"$($iArg.dependsOn | Select-Object -First 1)`", func() (*c8y.Client, error) { return ccmd.factory.Client()}),") } - "(\[\])?firmware(name)?$" { [void] $CompletionBuilderOptions.AppendLine("completion.WithFirmware(`"$($iArg.Name)`", func() (*c8y.Client, error) { return ccmd.factory.Client()}),") } - "(\[\])?firmwareversion(name)?$" { [void] $CompletionBuilderOptions.AppendLine("completion.WithFirmwareVersion(`"$($iArg.Name)`", `"$($iArg.dependsOn | Select-Object -First 1)`", func() (*c8y.Client, error) { return ccmd.factory.Client()}),") } - "(\[\])?firmwarepatch(name)?$" { [void] $CompletionBuilderOptions.AppendLine("completion.WithFirmwarePatch(`"$($iArg.Name)`", `"$($iArg.dependsOn | Select-Object -First 1)`", func() (*c8y.Client, error) { return ccmd.factory.Client()}),") } - "(\[\])?configuration$" { [void] $CompletionBuilderOptions.AppendLine("completion.WithConfiguration(`"$($iArg.Name)`", func() (*c8y.Client, error) { return ccmd.factory.Client()}),") } - "(\[\])?deviceprofile$" { [void] $CompletionBuilderOptions.AppendLine("completion.WithDeviceProfile(`"$($iArg.Name)`", func() (*c8y.Client, error) { return ccmd.factory.Client()}),") } - "(\[\])?deviceservice$" { [void] $CompletionBuilderOptions.AppendLine("completion.WithDeviceService(`"$($iArg.Name)`", `"$($iArg.dependsOn | Select-Object -First 1)`", func() (*c8y.Client, error) { return ccmd.factory.Client()}),") } - "(\[\])?certificate$" { [void] $CompletionBuilderOptions.AppendLine("completion.WithDeviceCertificate(`"$($iArg.Name)`", func() (*c8y.Client, error) { return ccmd.factory.Client()}),") } - "subscriptionName$" { [void] $CompletionBuilderOptions.AppendLine("completion.WithNotification2SubscriptionName(`"$($iArg.Name)`", func() (*c8y.Client, error) { return ccmd.factory.Client()}),") } - "subscriptionId$" { [void] $CompletionBuilderOptions.AppendLine("completion.WithNotification2SubscriptionId(`"$($iArg.Name)`", func() (*c8y.Client, error) { return ccmd.factory.Client()}),") } + "microservice" { [void] $CompletionBuilderOptions.AppendLine("completion.WithMicroservice(`"$($iArg.Name)`", func() (*c8y.Client, error) { return ccmd.factory.Client()}),") } + "microservicename" { [void] $CompletionBuilderOptions.AppendLine("completion.WithMicroservice(`"$($iArg.Name)`", func() (*c8y.Client, error) { return ccmd.factory.Client()}),") } + "microserviceinstance" { [void] $CompletionBuilderOptions.AppendLine("completion.WithMicroserviceInstance(`"$($iArg.Name)`", `"id`", func() (*c8y.Client, error) { return ccmd.factory.Client()}),") } + { @("role[]", "roleself[]") -contains $_ } { [void] $CompletionBuilderOptions.AppendLine("completion.WithUserRole(`"$($iArg.Name)`", func() (*c8y.Client, error) { return ccmd.factory.Client()}),") } + "devicerequest[]" { [void] $CompletionBuilderOptions.AppendLine("completion.WithDeviceRegistrationRequest(`"$($iArg.Name)`", func() (*c8y.Client, error) { return ccmd.factory.Client()}),") } + { @("user[]", "userself[]") -contains $_ } { [void] $CompletionBuilderOptions.AppendLine("completion.WithUser(`"$($iArg.Name)`", func() (*c8y.Client, error) { return ccmd.factory.Client()}),") } + "usergroup[]" { [void] $CompletionBuilderOptions.AppendLine("completion.WithUserGroup(`"$($iArg.Name)`", func() (*c8y.Client, error) { return ccmd.factory.Client()}),") } + "devicegroup[]" { [void] $CompletionBuilderOptions.AppendLine("completion.WithDeviceGroup(`"$($iArg.Name)`", func() (*c8y.Client, error) { return ccmd.factory.Client()}),") } + "smartgroup[]" { [void] $CompletionBuilderOptions.AppendLine("completion.WithSmartGroup(`"$($iArg.Name)`", func() (*c8y.Client, error) { return ccmd.factory.Client()}),") } + { @("tenant", "tenantname") -contains $_ } { [void] $CompletionBuilderOptions.AppendLine("completion.WithTenantID(`"$($iArg.Name)`", func() (*c8y.Client, error) { return ccmd.factory.Client()}),") } + "device[]" { [void] $CompletionBuilderOptions.AppendLine("completion.WithDevice(`"$($iArg.Name)`", func() (*c8y.Client, error) { return ccmd.factory.Client()}),") } + "agent[]" { [void] $CompletionBuilderOptions.AppendLine("completion.WithAgent(`"$($iArg.Name)`", func() (*c8y.Client, error) { return ccmd.factory.Client()}),") } + { @("software[]", "softwareName") -contains $_ } { [void] $CompletionBuilderOptions.AppendLine("completion.WithSoftware(`"$($iArg.Name)`", func() (*c8y.Client, error) { return ccmd.factory.Client()}),") } + { @("softwareversion[]", "softwareversionName") -contains $_ } { [void] $CompletionBuilderOptions.AppendLine("completion.WithSoftwareVersion(`"$($iArg.Name)`", `"$($iArg.dependsOn | Select-Object -First 1)`", func() (*c8y.Client, error) { return ccmd.factory.Client()}),") } + { @("firmware[]", "firmwareName") -contains $_ } { [void] $CompletionBuilderOptions.AppendLine("completion.WithFirmware(`"$($iArg.Name)`", func() (*c8y.Client, error) { return ccmd.factory.Client()}),") } + { @("firmwareversion[]", "firmwareversionName") -contains $_ } { [void] $CompletionBuilderOptions.AppendLine("completion.WithFirmwareVersion(`"$($iArg.Name)`", `"$($iArg.dependsOn | Select-Object -First 1)`", func() (*c8y.Client, error) { return ccmd.factory.Client()}),") } + { @("firmwarepatch[]", "firmwarepatchName") -contains $_ } { [void] $CompletionBuilderOptions.AppendLine("completion.WithFirmwarePatch(`"$($iArg.Name)`", `"$($iArg.dependsOn | Select-Object -First 1)`", func() (*c8y.Client, error) { return ccmd.factory.Client()}),") } + "configuration[]" { [void] $CompletionBuilderOptions.AppendLine("completion.WithConfiguration(`"$($iArg.Name)`", func() (*c8y.Client, error) { return ccmd.factory.Client()}),") } + "deviceprofile[]" { [void] $CompletionBuilderOptions.AppendLine("completion.WithDeviceProfile(`"$($iArg.Name)`", func() (*c8y.Client, error) { return ccmd.factory.Client()}),") } + "deviceservice[]" { [void] $CompletionBuilderOptions.AppendLine("completion.WithDeviceService(`"$($iArg.Name)`", `"$($iArg.dependsOn | Select-Object -First 1)`", func() (*c8y.Client, error) { return ccmd.factory.Client()}),") } + "certificate[]" { [void] $CompletionBuilderOptions.AppendLine("completion.WithDeviceCertificate(`"$($iArg.Name)`", func() (*c8y.Client, error) { return ccmd.factory.Client()}),") } + "subscriptionName" { [void] $CompletionBuilderOptions.AppendLine("completion.WithNotification2SubscriptionName(`"$($iArg.Name)`", func() (*c8y.Client, error) { return ccmd.factory.Client()}),") } + "subscriptionId" { [void] $CompletionBuilderOptions.AppendLine("completion.WithNotification2SubscriptionId(`"$($iArg.Name)`", func() (*c8y.Client, error) { return ccmd.factory.Client()}),") } } $ArgParams = @{ @@ -363,7 +362,7 @@ # Add support for user defined templates to control body # if ($Specification.bodyTemplates.type -ne "none") { - $null = $RESTBodyBuilderOptions.AppendLine("cmdutil.WithTemplateValue(cfg),") + $null = $RESTBodyBuilderOptions.AppendLine("cmdutil.WithTemplateValue(n.factory),") $null = $RESTBodyBuilderOptions.AppendLine("flags.WithTemplateVariablesValue(),") } @@ -828,24 +827,6 @@ Function Get-C8yGoArgs { } $Entry = switch ($Type) { - "id" { - # TODO: refactor addIdFlag to accept a property name - if ($Name -eq "id") { - @{ - SetFlag = "addIDFlag(cmd)" - } - } else { - $SetFlag = if ($UseOption) { - 'cmd.Flags().StringP("{0}", "{1}", "{2}", "{3}")' -f $Name, $OptionName, $Default, $Description - } else { - 'cmd.Flags().String("{0}", "{1}", "{2}")' -f $Name, $Default, $Description - } - @{ - SetFlag = $SetFlag - } - } - } - "json" { @{ SetFlagOptions = @( @@ -904,7 +885,7 @@ Function Get-C8yGoArgs { } } - "[]string" { + "string[]" { $SetFlag = if ($UseOption) { "cmd.Flags().StringSlice(`"${Name}`", `"${OptionName}`", []string{`"${Default}`"}, `"${Description}`")" } else { @@ -915,7 +896,7 @@ Function Get-C8yGoArgs { } } - "[]stringcsv" { + "stringcsv[]" { $SetFlag = if ($UseOption) { "cmd.Flags().StringSlice(`"${Name}`", `"${OptionName}`", []string{`"${Default}`"}, `"${Description}`")" } else { @@ -927,7 +908,7 @@ Function Get-C8yGoArgs { break } - "[]device" { + "device[]" { $SetFlag = if ($UseOption) { "cmd.Flags().StringSliceP(`"${Name}`", []string{`"${Default}`"}, `"${OptionName}`", `"${Description}`")" } else { @@ -940,7 +921,7 @@ Function Get-C8yGoArgs { } } - "[]agent" { + "agent[]" { $SetFlag = if ($UseOption) { "cmd.Flags().StringSliceP(`"${Name}`", []string{`"${Default}`"}, `"${OptionName}`", `"${Description}`")" } else { @@ -954,7 +935,7 @@ Function Get-C8yGoArgs { } # Management repository types - { $_ -in "[]software", "[]softwareversion", "[]firmware", "[]firmwareversion", "[]firmwarepatch", "[]configuration", "[]deviceprofile" } { + { $_ -in "software[]", "softwareversion[]", "firmware[]", "firmwareversion[]", "firmwarepatch[]", "configuration[]", "deviceprofile[]" } { $SetFlag = if ($UseOption) { "cmd.Flags().StringSliceP(`"${Name}`", []string{`"${Default}`"}, `"${OptionName}`", `"${Description}`")" } else { @@ -967,7 +948,7 @@ Function Get-C8yGoArgs { } # Device extensions - { $_ -in @("[]deviceservice") } { + { $_ -in @("deviceservice[]") } { $SetFlag = if ($UseOption) { "cmd.Flags().StringSliceP(`"${Name}`", []string{`"${Default}`"}, `"${OptionName}`", `"${Description}`")" } else { @@ -1003,7 +984,7 @@ Function Get-C8yGoArgs { } } - "[]devicegroup" { + "devicegroup[]" { $SetFlag = if ($UseOption) { "cmd.Flags().StringSliceP(`"${Name}`", []string{`"${Default}`"}, `"${OptionName}`", `"${Description}`")" } else { @@ -1016,7 +997,7 @@ Function Get-C8yGoArgs { } } - "[]id" { + "id[]" { $SetFlag = if ($UseOption) { "cmd.Flags().StringSliceP(`"${Name}`", []string{`"${Default}`"}, `"${OptionName}`", `"${Description}`")" } else { @@ -1028,7 +1009,7 @@ Function Get-C8yGoArgs { } } - "[]smartgroup" { + "smartgroup[]" { $SetFlag = if ($UseOption) { "cmd.Flags().StringSliceP(`"${Name}`", []string{`"${Default}`"}, `"${OptionName}`", `"${Description}`")" } else { @@ -1041,7 +1022,7 @@ Function Get-C8yGoArgs { } } - "[]roleself" { + "roleself[]" { $SetFlag = if ($UseOption) { "cmd.Flags().StringSliceP(`"${Name}`", []string{`"${Default}`"}, `"${OptionName}`", `"${Description}`")" } else { @@ -1054,7 +1035,7 @@ Function Get-C8yGoArgs { } } - "[]role" { + "role[]" { $SetFlag = if ($UseOption) { "cmd.Flags().StringSliceP(`"${Name}`", []string{`"${Default}`"}, `"${OptionName}`", `"${Description}`")" } else { @@ -1067,7 +1048,7 @@ Function Get-C8yGoArgs { } } - "[]usergroup" { + "usergroup[]" { $SetFlag = if ($UseOption) { "cmd.Flags().StringSliceP(`"${Name}`", []string{`"${Default}`"}, `"${OptionName}`", `"${Description}`")" } else { @@ -1176,7 +1157,7 @@ Function Get-C8yGoArgs { } } - "[]devicerequest" { + "devicerequest[]" { $SetFlag = if ($UseOption) { "cmd.Flags().StringSliceP(`"${Name}`", []string{`"${Default}`"}, `"${OptionName}`", `"${Description}`")" } else { @@ -1288,7 +1269,7 @@ Function Get-C8yGoArgs { } } - "[]user" { + "user[]" { $SetFlag = if ($UseOption) { "cmd.Flags().StringSliceP(`"${Name}`", []string{`"${Default}`"}, `"${OptionName}`", `"${Description}`")" } else { @@ -1300,7 +1281,7 @@ Function Get-C8yGoArgs { } } - "[]userself" { + "userself[]" { $SetFlag = if ($UseOption) { "cmd.Flags().StringSliceP(`"${Name}`", []string{`"${Default}`"}, `"${OptionName}`", `"${Description}`")" } else { @@ -1313,7 +1294,7 @@ Function Get-C8yGoArgs { } # Trusted device certficates - "[]certificate" { + "certificate[]" { $SetFlag = if ($UseOption) { "cmd.Flags().StringSlice(`"${Name}`", `"${OptionName}`", []string{`"${Default}`"}, `"${Description}`")" } else { diff --git a/scripts/build-cli/New-C8yApiGoGetValueFromFlag.ps1 b/scripts/build-cli/New-C8yApiGoGetValueFromFlag.ps1 index 760c25b03..af07f18aa 100644 --- a/scripts/build-cli/New-C8yApiGoGetValueFromFlag.ps1 +++ b/scripts/build-cli/New-C8yApiGoGetValueFromFlag.ps1 @@ -31,7 +31,7 @@ $Definitions = @{ # file (used in multipart/form-data uploads). It writes to the formData object instead of the body - "file" = "flags.WithFormDataFileAndInfoWithTemplateSupport(cmdutil.NewTemplateResolver(cfg), `"${prop}`", `"data`")...," + "file" = "flags.WithFormDataFileAndInfoWithTemplateSupport(cmdutil.NewTemplateResolver(n.factory), `"${prop}`", `"data`")...," # fileContents. File contents will be added to body "fileContents" = "flags.WithFilePath(`"${prop}`", `"${queryParam}`", `"$FixedValue`")," @@ -55,10 +55,10 @@ "date" = "flags.WithRelativeDate(false, `"${prop}`", `"${queryParam}`"$FormatValue)," # string array/slice - "[]string" = "flags.WithStringSliceValues(`"${prop}`", `"${queryParam}`", `"$FixedValue`")," + "string[]" = "flags.WithStringSliceValues(`"${prop}`", `"${queryParam}`", `"$FixedValue`")," # string array/slice as a comma separated string - "[]stringcsv" = "flags.WithStringSliceCSV(`"${prop}`", `"${queryParam}`", `"$FixedValue`")," + "stringcsv[]" = "flags.WithStringSliceCSV(`"${prop}`", `"${queryParam}`", `"$FixedValue`")," # inventoryChildType "inventoryChildType" = "flags.WithInventoryChildType(`"${prop}`", `"${queryParam}`"$FormatValue)," @@ -82,13 +82,13 @@ "json_custom" = "flags.WithDataValue(`"${prop}`", `"${queryParam}`"$FormatValue)," # binaryUploadURL: uploads a binary and returns the URL - "binaryUploadURL" = "c8ybinary.WithBinaryUploadURL(client, n.factory.IOStreams.ProgressIndicator(), `"${prop}`", `"${queryParam}`"$FormatValue)," + "binaryUploadURL" = "c8ybinary.WithBinaryUploadURL(n.factory.Client, n.factory.IOStreams.ProgressIndicator(), `"${prop}`", `"${queryParam}`"$FormatValue)," # json - don't do anything because it should be manually set "json" = "" # tenant - "tenant" = "flags.WithStringDefaultValue(client.TenantName, `"${prop}`", `"${queryParam}`"$FormatValue)," + "tenant" = "flags.WithStringDefaultValue(n.factory.GetTenant(), `"${prop}`", `"${queryParam}`"$FormatValue)," # tenantname (optional) "tenantname" = "flags.WithStringValue(`"${prop}`", `"${queryParam}`"$FormatValue)," @@ -98,16 +98,16 @@ "subscriptionId" = "flags.WithStringValue(`"${prop}`", `"${queryParam}`"$FormatValue)," # application - "application" = "c8yfetcher.WithApplicationByNameFirstMatch(client, args, `"${prop}`", `"${queryParam}`"$FormatValue)," + "application" = "c8yfetcher.WithApplicationByNameFirstMatch(n.factory, args, `"${prop}`", `"${queryParam}`"$FormatValue)," # hostedapplication (web app) - "hostedapplication" = "c8yfetcher.WithHostedApplicationByNameFirstMatch(client, args, `"${prop}`", `"${queryParam}`"$FormatValue)," + "hostedapplication" = "c8yfetcher.WithHostedApplicationByNameFirstMatch(n.factory, args, `"${prop}`", `"${queryParam}`"$FormatValue)," # applicationname "applicationname" = "flags.WithStringValue(`"${prop}`", `"${queryParam}`"$FormatValue)," # microservice - "microservice" = "c8yfetcher.WithMicroserviceByNameFirstMatch(client, args, `"${prop}`", `"${queryParam}`"$FormatValue)," + "microservice" = "c8yfetcher.WithMicroserviceByNameFirstMatch(n.factory, args, `"${prop}`", `"${queryParam}`"$FormatValue)," # microservice instance "microserviceinstance" = "flags.WithStringValue(`"${prop}`", `"${queryParam}`"$FormatValue)," @@ -116,29 +116,29 @@ "microservicename" = "flags.WithStringValue(`"${prop}`", `"${queryParam}`"$FormatValue)," # devicerequest array - "[]devicerequest" = "c8yfetcher.WithIDSlice(args, `"${prop}`", `"${queryParam}`"$FormatValue)," + "devicerequest[]" = "c8yfetcher.WithIDSlice(args, `"${prop}`", `"${queryParam}`"$FormatValue)," # id array - "[]id" = "c8yfetcher.WithIDSlice(args, `"${prop}`", `"${queryParam}`"$FormatValue)," + "id[]" = "c8yfetcher.WithIDSlice(args, `"${prop}`", `"${queryParam}`"$FormatValue)," # software array - "[]software" = "c8yfetcher.WithSoftwareByNameFirstMatch(client, args, `"${prop}`", `"${queryParam}`"$FormatValue)," + "software[]" = "c8yfetcher.WithSoftwareByNameFirstMatch(n.factory, args, `"${prop}`", `"${queryParam}`"$FormatValue)," "softwareDetails" = @( - "c8yfetcher.WithSoftwareVersionData(client, `"software`", `"version`", `"url`", args, `"`", `"${queryParam}`"$FormatValue)," + "c8yfetcher.WithSoftwareVersionData(n.factory, `"software`", `"version`", `"url`", args, `"`", `"${queryParam}`"$FormatValue)," ) -join "`n" "configurationDetails" = @( - "c8yfetcher.WithConfigurationFileData(client, `"configuration`", `"configurationType`", `"url`", args, `"`", `"${queryParam}`"$FormatValue)," + "c8yfetcher.WithConfigurationFileData(n.factory, `"configuration`", `"configurationType`", `"url`", args, `"`", `"${queryParam}`"$FormatValue)," ) -join "`n" # software name "softwareName" = "flags.WithStringValue(`"${prop}`", `"${queryParam}`"$FormatValue)," # software version array - "[]softwareversion" = "c8yfetcher.WithSoftwareVersionByNameFirstMatch(client, args, `"${prop}`", `"${queryParam}`"$FormatValue)," + "softwareversion[]" = "c8yfetcher.WithSoftwareVersionByNameFirstMatch(n.factory, `"software`", args, `"${prop}`", `"${queryParam}`"$FormatValue)," - "[]deviceservice" = "c8yfetcher.WithDeviceServiceByNameFirstMatch(client, args, `"${prop}`", `"${queryParam}`"$FormatValue)," + "deviceservice[]" = "c8yfetcher.WithDeviceServiceByNameFirstMatch(n.factory, `"device`", args, `"${prop}`", `"${queryParam}`"$FormatValue)," # software version name "softwareversionName" = "flags.WithStringValue(`"${prop}`", `"${queryParam}`"$FormatValue)," @@ -147,64 +147,64 @@ "certificatefile" = "flags.WithCertificateFile(`"${prop}`", `"${queryParam}`")," # Certificate file - "[]certificate" = "c8yfetcher.WithCertificateByNameFirstMatch(client, args, `"${prop}`", `"${queryParam}`")," + "certificate[]" = "c8yfetcher.WithCertificateByNameFirstMatch(n.factory, args, `"${prop}`", `"${queryParam}`")," # firmware array - "[]firmware" = "c8yfetcher.WithFirmwareByNameFirstMatch(client, args, `"${prop}`", `"${queryParam}`"$FormatValue)," + "firmware[]" = "c8yfetcher.WithFirmwareByNameFirstMatch(n.factory, args, `"${prop}`", `"${queryParam}`"$FormatValue)," # firmware name "firmwareName" = "flags.WithStringValue(`"${prop}`", `"${queryParam}`"$FormatValue)," # firmware version array - "[]firmwareversion" = "c8yfetcher.WithFirmwareVersionByNameFirstMatch(client, args, `"${prop}`", `"${queryParam}`"$FormatValue)," + "firmwareversion[]" = "c8yfetcher.WithFirmwareVersionByNameFirstMatch(n.factory, `"firmware`", args, `"${prop}`", `"${queryParam}`"$FormatValue)," # firmware version name "firmwareversionName" = "flags.WithStringValue(`"${prop}`", `"${queryParam}`"$FormatValue)," "firmwareDetails" = @( - "c8yfetcher.WithFirmwareVersionData(client, `"firmware`", `"version`", `"url`", args, `"`", `"${queryParam}`")," + "c8yfetcher.WithFirmwareVersionData(n.factory, `"firmware`", `"version`", `"url`", args, `"`", `"${queryParam}`")," ) -join "`n" # firmware version patch array - "[]firmwarepatch" = "c8yfetcher.WithFirmwarePatchByNameFirstMatch(client, args, `"${prop}`", `"${queryParam}`")," + "firmwarepatch[]" = "c8yfetcher.WithFirmwarePatchByNameFirstMatch(n.factory, `"firmware`", args, `"${prop}`", `"${queryParam}`")," # firmware patch name "firmwarepatchName" = "flags.WithStringValue(`"${prop}`", `"${queryParam}`")," # configuration array - "[]configuration" = "c8yfetcher.WithConfigurationByNameFirstMatch(client, args, `"${prop}`", `"${queryParam}`")," + "configuration[]" = "c8yfetcher.WithConfigurationByNameFirstMatch(n.factory, args, `"${prop}`", `"${queryParam}`")," # deviceprofile array - "[]deviceprofile" = "c8yfetcher.WithDeviceProfileByNameFirstMatch(client, args, `"${prop}`", `"${queryParam}`")," + "deviceprofile[]" = "c8yfetcher.WithDeviceProfileByNameFirstMatch(n.factory, args, `"${prop}`", `"${queryParam}`")," # device array - "[]device" = "c8yfetcher.WithDeviceByNameFirstMatch(client, args, `"${prop}`", `"${queryParam}`"$FormatValue)," + "device[]" = "c8yfetcher.WithDeviceByNameFirstMatch(n.factory, args, `"${prop}`", `"${queryParam}`"$FormatValue)," # agent array - "[]agent" = "c8yfetcher.WithAgentByNameFirstMatch(client, args, `"${prop}`", `"${queryParam}`"$FormatValue)," + "agent[]" = "c8yfetcher.WithAgentByNameFirstMatch(n.factory, args, `"${prop}`", `"${queryParam}`"$FormatValue)," # devicegroup array - "[]devicegroup" = "c8yfetcher.WithDeviceGroupByNameFirstMatch(client, args, `"${prop}`", `"${queryParam}`"$FormatValue)," + "devicegroup[]" = "c8yfetcher.WithDeviceGroupByNameFirstMatch(n.factory, args, `"${prop}`", `"${queryParam}`"$FormatValue)," # smartgroup array - "[]smartgroup" = "c8yfetcher.WithSmartGroupByNameFirstMatch(client, args, `"${prop}`", `"${queryParam}`"$FormatValue)," + "smartgroup[]" = "c8yfetcher.WithSmartGroupByNameFirstMatch(n.factory, args, `"${prop}`", `"${queryParam}`"$FormatValue)," # user array - "[]user" = "c8yfetcher.WithUserByNameFirstMatch(client, args, `"${prop}`", `"${queryParam}`"$FormatValue)," + "user[]" = "c8yfetcher.WithUserByNameFirstMatch(n.factory, args, `"${prop}`", `"${queryParam}`"$FormatValue)," # user self url array - "[]userself" = "c8yfetcher.WithUserSelfByNameFirstMatch(client, args, `"${prop}`", `"${queryParam}`"$FormatValue)," + "userself[]" = "c8yfetcher.WithUserSelfByNameFirstMatch(n.factory, args, `"${prop}`", `"${queryParam}`"$FormatValue)," # role self url array - "[]roleself" = "c8yfetcher.WithRoleSelfByNameFirstMatch(client, args, `"${prop}`", `"${queryParam}`"$FormatValue)," + "roleself[]" = "c8yfetcher.WithRoleSelfByNameFirstMatch(n.factory, args, `"${prop}`", `"${queryParam}`"$FormatValue)," # role array - "[]role" = "c8yfetcher.WithRoleByNameFirstMatch(client, args, `"${prop}`", `"${queryParam}`"$FormatValue)," + "role[]" = "c8yfetcher.WithRoleByNameFirstMatch(n.factory, args, `"${prop}`", `"${queryParam}`"$FormatValue)," # user group array - "[]usergroup" = "c8yfetcher.WithUserGroupByNameFirstMatch(client, args, `"${prop}`", `"${queryParam}`"$FormatValue)," + "usergroup[]" = "c8yfetcher.WithUserGroupByNameFirstMatch(n.factory, args, `"${prop}`", `"${queryParam}`"$FormatValue)," } diff --git a/scripts/build-cli/New-C8yApiGoRootCommand.ps1 b/scripts/build-cli/New-C8yApiGoRootCommand.ps1 index 910524549..980b8ce08 100644 --- a/scripts/build-cli/New-C8yApiGoRootCommand.ps1 +++ b/scripts/build-cli/New-C8yApiGoRootCommand.ps1 @@ -10,10 +10,10 @@ [string] $OutputDir = "./" ) - $Name = $Specification.information.name.ToLower() - $BaseName = Split-Path -Path $Specification.information.name.ToLower() -Leaf + $Name = $Specification.group.name.ToLower() + $BaseName = Split-Path -Path $Specification.group.name.ToLower() -Leaf - if ($Specification.information.skip -eq $true) { + if ($Specification.group.skip -eq $true) { Write-Information "Specification is marked to be ignored" return } @@ -24,9 +24,9 @@ } $BaseNameLowercase = $BaseName.ToLower() $NameCamel = $BaseNameLowercase[0].ToString().ToUpperInvariant() + $BaseNameLowercase.Substring(1) - $Description = $Specification.information.description - $DescriptionLong = $Specification.information.descriptionLong - $Hidden = $Specification.information.hidden + $Description = $Specification.group.description + $DescriptionLong = $Specification.group.descriptionLong + $Hidden = $Specification.group.hidden $SubcommandsCode = New-Object System.Text.StringBuilder $RootImportCode = New-Object System.Text.StringBuilder @@ -35,7 +35,7 @@ $File = Join-Path -Path $OutputDir -ChildPath ("{0}.auto.go" -f $BaseNameLowercase) - foreach ($endpoint in $Specification.endpoints) { + foreach ($endpoint in $Specification.commands) { if ($endpoint.skip -eq $true) { Write-Verbose ("Skipping [{0}]" -f $endpoint.name) continue diff --git a/scripts/build-powershell/New-C8yApiPowershellCommand.ps1 b/scripts/build-powershell/New-C8yApiPowershellCommand.ps1 index e234a5e0e..cf0a1e8e6 100644 --- a/scripts/build-powershell/New-C8yApiPowershellCommand.ps1 +++ b/scripts/build-powershell/New-C8yApiPowershellCommand.ps1 @@ -225,7 +225,7 @@ break } - "[]stringcsv" { + "stringcsv[]" { "`${0} -join ','" -f $item.Name break } @@ -382,12 +382,12 @@ $null = $RESTQueryBuilder.AppendLine("") } - "[]device" { + "device[]" { $null = $RESTQueryBuilder.AppendLine("") } # Array of strings - "[]string" { + "string[]" { $null = $RESTQueryBuilder.AppendLine("") } @@ -575,13 +575,13 @@ Function Get-IteratorFunction { ) $ExpandFunction = switch ($Type) { - "[]device" { "(PSc8y\Expand-Device $Variable)" } - "[]id" { "(PSc8y\Expand-Id $Variable)" } - "[]role" { "(PSc8y\Expand-Id $Variable)" } - "[]roleself" { "(PSc8y\Expand-Id $Variable)" } - "[]tenant" { "(PSc8y\Expand-Tenant $Variable)" } - "[]userself" { "(PSc8y\Expand-User $Variable)" } - "[]user" { "(PSc8y\Expand-User $Variable)" } + "device[]" { "(PSc8y\Expand-Device $Variable)" } + "id[]" { "(PSc8y\Expand-Id $Variable)" } + "role[]" { "(PSc8y\Expand-Id $Variable)" } + "roleself[]" { "(PSc8y\Expand-Id $Variable)" } + "tenant[]" { "(PSc8y\Expand-Tenant $Variable)" } + "userself[]" { "(PSc8y\Expand-User $Variable)" } + "user[]" { "(PSc8y\Expand-User $Variable)" } "application" { "(PSc8y\Expand-Application $Variable)" } "hostedapplication" { "(PSc8y\Expand-Application $Variable)" } "microservice" { "(PSc8y\Expand-Microservice $Variable)" } diff --git a/scripts/build-powershell/New-C8yPowershellApi.ps1 b/scripts/build-powershell/New-C8yPowershellApi.ps1 index 1739ba81f..8dbdc4395 100644 --- a/scripts/build-powershell/New-C8yPowershellApi.ps1 +++ b/scripts/build-powershell/New-C8yPowershellApi.ps1 @@ -26,12 +26,12 @@ $Specification = Get-Content $Path -Raw -Encoding utf8 | ConvertFrom-Json - if ($Specification.information.skip -eq $true) { + if ($Specification.group.skip -eq $true) { Write-Verbose ("Skipping [{0}]" -f $Path) continue } - foreach ($iSpec in $Specification.endpoints) { + foreach ($iSpec in $Specification.commands) { if ($iSpec.skip -eq $true) { Write-Verbose ("Skipping [{0}]" -f $iSpec.name) continue @@ -44,7 +44,7 @@ New-C8yApiPowershellCommand ` -Specification:$iSpec ` - -Noun $Specification.information.name ` + -Noun $Specification.group.name ` -OutputDir:$OutputDir } } diff --git a/scripts/build-powershell/New-C8yPowershellArguments.ps1 b/scripts/build-powershell/New-C8yPowershellArguments.ps1 index a87e8ca34..1abdc3567 100644 --- a/scripts/build-powershell/New-C8yPowershellArguments.ps1 +++ b/scripts/build-powershell/New-C8yPowershellArguments.ps1 @@ -51,29 +51,29 @@ # Type Definition $DataType = switch ($Type) { - "[]agent" { "object[]"; break } - "[]certificate" { "object[]"; break } - "[]configuration" { "object[]"; break } - "[]device" { "object[]"; break } - "[]devicegroup" { "object[]"; break } - "[]deviceprofile" { "object[]"; break } - "[]devicerequest" { "object[]"; break } - "[]firmware" { "object[]"; break } - "[]firmwarepatch" { "object[]"; break } - "[]firmwareversion" { "object[]"; break } - "[]id" { "object[]"; break } - "[]role" { "object[]"; break } - "[]roleself" { "object[]"; break } - "[]smartgroup" { "object[]"; break } - "[]software" { "object[]"; break } - "[]softwareversion" { "object[]"; break } - "[]deviceservice" { "object[]"; break } - "[]string" { "string[]"; break } - "[]stringcsv" { "string[]"; break } + "agent[]" { "object[]"; break } + "certificate[]" { "object[]"; break } + "configuration[]" { "object[]"; break } + "device[]" { "object[]"; break } + "devicegroup[]" { "object[]"; break } + "deviceprofile[]" { "object[]"; break } + "devicerequest[]" { "object[]"; break } + "firmware[]" { "object[]"; break } + "firmwarepatch[]" { "object[]"; break } + "firmwareversion[]" { "object[]"; break } + "id[]" { "object[]"; break } + "role[]" { "object[]"; break } + "roleself[]" { "object[]"; break } + "smartgroup[]" { "object[]"; break } + "software[]" { "object[]"; break } + "softwareversion[]" { "object[]"; break } + "deviceservice[]" { "object[]"; break } + "string[]" { "string[]"; break } + "stringcsv[]" { "string[]"; break } "[]tenant" { "object[]"; break } - "[]user" { "object[]"; break } - "[]usergroup" { "object[]"; break } - "[]userself" { "object[]"; break } + "user[]" { "object[]"; break } + "usergroup[]" { "object[]"; break } + "userself[]" { "object[]"; break } "application" { "object[]"; break } "applicationname" { "string"; break } "attachment" { "string"; break } diff --git a/tests/manual/alias/execute/alias_execute.yaml b/tests/manual/alias/execute/alias_execute.yaml index 626ee51cb..f9751996e 100644 --- a/tests/manual/alias/execute/alias_execute.yaml +++ b/tests/manual/alias/execute/alias_execute.yaml @@ -37,6 +37,7 @@ tests: C8Y_SESSION: '/tmp/.cumulocity-alias/mysession.yaml' C8Y_SESSION_HOME: '/tmp/.cumulocity-alias' C8Y_HOME: '/tmp/.cumulocity-alias' + C8Y_SETTINGS_EXTENSIONS_DATADIR: '/tmp/go-c8y-cli' command: | mkdir -p "$C8Y_HOME" rm -f "$C8Y_SESSION" diff --git a/tests/manual/common/errors/errors_command.yaml b/tests/manual/common/errors/errors_command.yaml index 77a32b9b6..d1002e64c 100644 --- a/tests/manual/common/errors/errors_command.yaml +++ b/tests/manual/common/errors/errors_command.yaml @@ -18,7 +18,7 @@ tests: It returns writes errors to stderr: command: | - c8y events get --iiiiid 0 + c8y events get --iiiiid 0 --noColor exit-code: 100 stderr: match-pattern: | @@ -28,7 +28,7 @@ tests: It returns errors as json on stdout: command: | - c8y events get --withError --iiiiid 0 + c8y events get --withError --iiiiid 0 --noColor exit-code: 100 stderr: match-pattern: "ERROR\\s+commandError: unknown flag: --iiiiid" diff --git a/tests/manual/extensions/crud.sh b/tests/manual/extensions/crud.sh new file mode 100755 index 000000000..237b8f757 --- /dev/null +++ b/tests/manual/extensions/crud.sh @@ -0,0 +1,62 @@ +#!/bin/bash + +set -ex + +export C8Y_SETTINGS_DEFAULTS_DRY=false + +createdir () { + # Cross-platform compatible + local name="${1:-"c8y-temp"}" + tmpdir=$(mktemp -d 2>/dev/null || mktemp -d -t "$name") + echo "$tmpdir" +} + +C8Y_SETTINGS_EXTENSIONS_DATADIR=$(createdir "datadir") +export C8Y_SETTINGS_EXTENSIONS_DATADIR +TEMP_DIR=$(createdir "extension") + +trap "rm -Rf $TEMP_DIR; rm -Rf $C8Y_SETTINGS_EXTENSIONS_DATADIR" EXIT + +cd "$TEMP_DIR" +EXTNAME="customext01" + +# Create +echo "Using extension name: $EXTNAME" +c8y extensions create "$EXTNAME" + +# Install from local repo +c8y extensions install "c8y-$EXTNAME" + +# Install from remote repo +c8y extensions install reubenmiller/c8y-defaults + +# List +c8y extensions list --select name -o csv --filter "name eq $EXTNAME" | grep -E "^$EXTNAME$" +c8y extensions list --select name -o csv --filter "name eq defaults" | grep -E "^defaults$" + +# Use command +OUTPUT=$(c8y customext01 services list 2>&1) +echo "$OUTPUT" | grep "Running custom services list command" + +# Use template +c8y inventory create --template "${EXTNAME}::customCommand.jsonnet" --dry +c8y __complete extensions delete "" | grep -E "^$EXTNAME$" + + +# Completion +c8y __complete extensions delete "" | grep -E "^$EXTNAME$" +c8y __complete extensions update "" | grep -E "^$EXTNAME$" +c8y __complete inventory create --template "$EXTNAME::" | grep "^$EXTNAME::customCommand.jsonnet$" +c8y __complete inventory list --view "$EXTNAME::" | grep "^$EXTNAME::customDevice" + +# Update +c8y extensions update "$EXTNAME" 2>&1 | grep -E "Failed updating extension $EXTNAME: local extensions can not be updated" +c8y extensions update --all + +# Delete +c8y extensions delete "$EXTNAME" +c8y extensions list --select name -o csv | grep -v "$EXTNAME" + +# TODO: Currently not supported +c8y extensions list | c8y extensions delete +[[ -z $(c8y extensions list --select name -o csv ) ]] diff --git a/tests/manual/extensions/example/body_basic_tests.yaml b/tests/manual/extensions/example/body_basic_tests.yaml new file mode 100644 index 000000000..3b796de3a --- /dev/null +++ b/tests/manual/extensions/example/body_basic_tests.yaml @@ -0,0 +1,272 @@ +# yaml-language-server: $schema=https://raw.githubusercontent.com/reubenmiller/commander/feat/handle-nested-files/schema.json + +config: + env: + C8Y_SETTINGS_DEFAULTS_CACHE: true + C8Y_SETTINGS_CACHE_METHODS: GET POST PUT + C8Y_SETTINGS_DEFAULTS_CACHETTL: 100h + C8Y_SETTINGS_DEFAULTS_DRY: true + C8Y_SETTINGS_DEFAULTS_DRY_FORMAT: json + +tests: + # + # Boolean + # + boolean: + command: | + c8y kitchensink body boolean --enable + stdout: + json: + body.enable: "true" + + boolean true: + command: | + c8y kitchensink body boolean --enable + stdout: + json: + body.enable: "true" + + boolean false: + command: | + c8y kitchensink body boolean --enable=false + stdout: + json: + body.enable: "false" + + boolean notset: + command: | + c8y kitchensink body boolean + stdout: + json: + body: "{}" + + booleanDefault: + command: | + c8y kitchensink body booleanDefault --enable + stdout: + json: + body.enable: "true" + + booleanDefault set: + command: | + c8y kitchensink body booleanDefault --active --enable + stdout: + json: + body.active: "true" + body.enable: "true" + + booleanDefault not-set: + command: | + c8y kitchensink body booleanDefault + stdout: + json: + body.active: "true" + body.enable: "false" + + booleanDefault false: + command: | + c8y kitchensink body booleanDefault --active=false --enable=false + stdout: + json: + body.active: "false" + body.enable: "false" + + optional_fragment: + command: | + c8y kitchensink body optional_fragment --enable + stdout: + json: + body.enable: "{}" + + optional_fragment (without): + command: | + c8y kitchensink body optional_fragment + stdout: + json: + body: "{}" + + # + # Date / Time + # + data date relative: + command: | + c8y kitchensink body date --dateFrom "0d" + stdout: + json: + body.dateFrom: r/^\d\d\d\d-\d\d-\d\d$ + + data date fixed: + command: | + c8y kitchensink body date --dateFrom "2023-03-31" + stdout: + json: + body.dateFrom: "2023-03-31" + + data datetime relative: + command: | + c8y kitchensink body datetime --dateFrom "-1h" + stdout: + json: + body.dateFrom: r/^\d\d\d\d-\d\d-\d\dT\d\d:\d\d:\d\d(.\d+)?(Z|[+-]\d\d:\d\d)$ + + data datetime fixed with no timezone: + command: | + c8y kitchensink body datetime --dateFrom "2023-03-31" + stdout: + json: + body.dateFrom: "2023-03-31T00:00:00Z" + + data datetime fixed with timezone: + command: | + c8y kitchensink body datetime --dateFrom "2023-03-31T00:00:00+02:00" + stdout: + json: + body.dateFrom: "2023-03-31T00:00:00+02:00" + + # + # Numbers + # + # + data integer positive: + command: | + c8y kitchensink body integer --value 10 + stdout: + json: + body.value: "10" + + data integer negative: + command: | + c8y kitchensink body integer --value -10 + stdout: + json: + body.value: "-10" + + data float positive: + command: | + c8y kitchensink body float --value 120.5 + stdout: + json: + body.value: "120.5" + + data float negative: + command: | + c8y kitchensink body float --value -10.5 + stdout: + json: + body.value: "-10.5" + + # + # String + # + string: + command: | + c8y kitchensink body string --value "hello world" + stdout: + json: + body.value: "hello world" + + stringStatic not-set: + command: | + c8y kitchensink body stringStatic + stdout: + json: + body.value: "some value" + + ? stringStatic set and it does not change value (as it should be hidden from the user) + : command: | + c8y kitchensink body stringStatic --value "some other value" + stdout: + json: + body.value: "some value" + + string[] as separate arguments: + command: | + c8y kitchensink body stringArray --value "hello" --value "world" + stdout: + json: + body.value.0: "hello" + body.value.1: "world" + + string[] as csv: + command: | + c8y kitchensink body stringArray --value "hello,world" + stdout: + json: + body.value.0: "hello" + body.value.1: "world" + + stringcsv[]: + command: | + c8y kitchensink body stringcsvArray --value "hello,world" --value again + stdout: + json: + body.value: "hello,world,again" + + # + # File based + # + file - It can upload a binary: + command: | + c8y kitchensink body file --file manual/extensions/file1.txt --dryFormat dump + stdout: + lines: + 8: 'Content-Disposition: form-data; name="file"; filename="file1.txt"' + 9: "Content-Type: application/octet-stream" + 10: "" + 11: one + 12: two + 13: three + 15: 'Content-Disposition: form-data; name="object"' + 16: "" + 17: '{"name":"file1.txt","type":"text/plain; charset=utf-8"}' + + fileContents - It can upload file contents: + command: | + c8y kitchensink body fileContents --file manual/extensions/file1.txt --dry --dryFormat json | + c8y util show --select body -o csv + stdout: + exactly: one\ntwo\nthree + + fileContentsAsString: + command: | + c8y kitchensink body fileContentsAsString --file manual/extensions/file1.txt --dry --dryFormat json | + c8y util show --select body + stdout: + exactly: | + {"body":{"file":"one\ntwo\nthree"}} + + attachment - multipart formdata upload without meta info: + command: | + c8y kitchensink body attachment --file manual/extensions/file1.txt --dry --dryFormat dump + stdout: + line-count-max: 14 + lines: + 5: "r/^Content-Type: multipart/form-data; boundary=[a-zA-Z0-9]+$" + 6: "" + 7: r/^--[a-zA-Z0-9]+$ + 8: 'Content-Disposition: form-data; name="file"; filename="file1.txt"' + 9: "Content-Type: application/octet-stream" + 10: "" + 11: one + 12: two + 13: three + 14: r/^--[a-zA-Z0-9]+--$ + + binaryUploadURL: + # File does not get deleted after test and dry mode does not work here + skip: true + command: | + c8y kitchensink body binaryUploadURL --file dummyfile + stdout: + json: + body.myUrl: r/https://.+/inventory/binaries/\d+$ + + # + # JSON + # + json_custom: + command: | + c8y kitchensink body json --mydata foo.bar=true + stdout: + json: + body.mydata.foo.bar: "true" diff --git a/tests/manual/extensions/example/body_cumulocity_tests.yaml b/tests/manual/extensions/example/body_cumulocity_tests.yaml new file mode 100644 index 000000000..3f5a06ada --- /dev/null +++ b/tests/manual/extensions/example/body_cumulocity_tests.yaml @@ -0,0 +1,347 @@ +# yaml-language-server: $schema=https://raw.githubusercontent.com/reubenmiller/commander/feat/handle-nested-files/schema.json + +config: + env: + C8Y_SETTINGS_DEFAULTS_CACHE: true + C8Y_SETTINGS_CACHE_METHODS: GET POST PUT + C8Y_SETTINGS_DEFAULTS_CACHETTL: 100h + C8Y_SETTINGS_DEFAULTS_DRY: true + C8Y_SETTINGS_DEFAULTS_DRY_FORMAT: json + +tests: + # + # Applications + # + application: + command: | + c8y kitchensink body_complex application --id administration + stdout: + json: + body.id: r/^[1-9][0-9]*$ + + applicationname: + command: | + c8y kitchensink body_complex applicationname --name cockpit + stdout: + json: + body.name: cockpit + + hostedapplication: + command: | + c8y kitchensink body_complex hostedapplication --id cockpit + stdout: + json: + body.id: r/^[1-9][0-9]*$ + + microservice: + command: | + c8y kitchensink body_complex microservice --id report-agent + stdout: + json: + body.id: r/^[1-9][0-9]*$ + + microservicename: + command: | + c8y kitchensink body_complex microservicename --name report-agent + stdout: + json: + body.name: report-agent + + microserviceinstance (completion): + # Only do completion check because lookups are not supported + command: | + c8y __complete kitchensink body_complex microserviceinstance --microservice advanced-software-mgmt --dry --instance "" + stdout: + lines: + 1: r/advanced-software-mgmt-scope.* + # 2: ":0" + # 3: "Completion ended with directive: ShellCompDirectiveDefault" + + # + # Devices / Agents / Sources + # + lookup source: + command: | + echo "1\n2" | c8y kitchensink body_complex source --id - | c8y util show --select body.id -o csv + stdout: + exactly: | + 1 + 2 + + lookup id: + command: | + c8y kitchensink body_complex idArray --id 1,2 3 4 5 + stdout: + json: + body.id.0: "1" + body.id.1: "2" + body.id.2: "3" + body.id.3: "4" + body.id.4: "5" + + lookup agent: + command: | + c8y kitchensink body_complex agent --id agent01 + stdout: + json: + body.id: r/^\d+$ + + lookup device: + command: | + c8y kitchensink body_complex device --id device01 + stdout: + json: + body.id: r/^\d+$ + + # + # Device Groups + # + lookup devicegroup: + command: | + c8y kitchensink body_complex devicegroup --id "My Group" + stdout: + json: + body.id: r/^\d+$ + + lookup smartgroup: + command: | + c8y kitchensink body_complex smartgroup --id "my smartgroup" + stdout: + json: + body.id: r/^\d+$ + + # + # Tenant + # + lookup tenant: + command: | + c8y kitchensink body_complex tenant --id $C8Y_TENANT + stdout: + json: + body.id: r/^$C8Y_TENANT$ + + lookup tenantname: + command: | + c8y kitchensink body_complex tenantname --name $C8Y_TENANT + stdout: + json: + body.name: r/^$C8Y_TENANT$ + + # + # Misc + # + lookup certificate: + command: | + c8y kitchensink body_complex certificate --id MyCert + stdout: + json: + body.id: r/^[0-9a-zA-Z]+$ + + lookup certificatefile: + command: | + c8y kitchensink body_complex certificatefile --file testdata/trustedcert.pem + stdout: + json: + body.file: MIIBoDCCAUagAwIBAgIISOLoBRYAHmwwCgYIKoZIzj0EAwIwPDEQMA4GA1UEAwwHdGVkZ2UwNDESMBAGA1UECgwJVGhpbiBFZGdlMRQwEgYDVQQLDAtUZXN0IERldmljZTAeFw0yMjA2MTcxMzQ3MzNaFw0yMzA2MTcxMzQ3MzNaMDwxEDAOBgNVBAMMB3RlZGdlMDQxEjAQBgNVBAoMCVRoaW4gRWRnZTEUMBIGA1UECwwLVGVzdCBEZXZpY2UwWTATBgcqhkjOPQIBBggqhkjOPQMBBwNCAASw2fovoPAzOtH8qH0KiAycbssZkuW0kRBXxLzp2XlBX7RFuXz9iATdANGIbhruG9AfwFu5Bm+5YDGEMfF9q/TWozIwMDAdBgNVHQ4EFgQUbB4AFgXo4kirlR9b4UVQ0DhcLC0wDwYDVR0TAQH/BAUwAwEB/zAKBggqhkjOPQQDAgNIADBFAiAwByF4Si/A6aUzmKdq5Ehj6ViAXrMtqN4MquxCZt5w6gIhAMNRdQZTFFocbI+Ybok+Gb35043kXLtQK7YgbQB/mYre + + # + # Device management + # + lookup device service: + skip: true + command: | + c8y kitchensink body_complex deviceservice --device device02 --service sshd + stdout: + json: + body.device: r/^\d+$ + body.service: r/^\d+$ + + # + # Device requests + # + lookup devicerequest: + command: | + c8y kitchensink body_complex devicerequest --id myspecialdevice + stdout: + json: + body.id: myspecialdevice + + lookup devicerequestArray: + command: | + c8y kitchensink body_complex devicerequestArray --id myspecialdevice + stdout: + json: + body.id.0: myspecialdevice + + # + # Users / User groups / Roles + # + lookup role: + command: | + c8y kitchensink body_complex role --role "ROLE_ALARM_REA*" + stdout: + json: + body.role: ROLE_ALARM_READ + + lookup roleself: + command: | + c8y kitchensink body_complex roleself --roleself "ROLE_ALARM_REA*" + stdout: + json: + body.roleself: r/^https://.+/user/roles/ROLE_ALARM_READ$ + + lookup user: + command: | + c8y kitchensink body_complex user --user "peterpi@example.com" + stdout: + json: + body.user: peterpi@example.com + + lookup userself: + command: | + c8y kitchensink body_complex userself --user "peterpi@example.com" + stdout: + json: + body.user: r/^https://.*/user/$C8Y_TENANT/users/peterpi@example.com$ + + lookup usergroup: + command: | + c8y kitchensink body_complex usergroup --group "admins" + stdout: + json: + body.group: r/^\d+$ + + # + # Repository + # + + # + # Configuration + # + lookup configuration: + command: | + c8y kitchensink body_complex configuration --id example-config + stdout: + json: + body.id: r/^\d+$ + + lookup configurationDetails: + command: | + c8y kitchensink body_complex configurationDetails --id example-config + stdout: + json: + body.c8y_Configuration.name: "example-config" + body.c8y_Configuration.type: "agentConfig" + body.c8y_Configuration.url: "https://test.com/content/raw/app.json" + + # + # Device profile + # + lookup deviceprofile: + command: | + c8y kitchensink body_complex deviceprofile --id profile01 + stdout: + json: + body.id: r/^\d+$ + + # + # Firmware + # + lookup firmware: + command: | + c8y kitchensink body_complex firmware --id iot-linux + stdout: + json: + body.id: r/^\d+$ + + lookup firmwarename: + command: | + c8y kitchensink body_complex firmwarename --name iot-linux + stdout: + json: + body.name: iot-linux + + lookup firmwareversion: + command: | + c8y kitchensink body_complex firmwareversion --sourceFirmware iot-linux --version 1.0.0 + stdout: + json: + body.sourceFirmware: r/^\d+$ + body.version: r/^\d+$ + + lookup firmwareversionName: + command: | + c8y kitchensink body_complex firmwareversionName --firmware iot-linux --version 1.0.0 + stdout: + json: + body.firmware: r/^\d+$ + body.version: "1.0.0" + + lookup firmwarepatch: + command: | + c8y kitchensink body_complex firmwarepatch --sourceFirmware iot-linux --patch 1.0.1 + stdout: + json: + body.sourceFirmware: r/^\d+$ + body.patch: r/^\d+$ + + lookup firmwarepatchName: + command: | + c8y kitchensink body_complex firmwarepatchName --firmware iot-linux --patch 1.0.1 + stdout: + json: + body.firmware: r/^\d+$ + body.patch: "1.0.1" + + lookup firmwareDetails: + command: | + c8y kitchensink body_complex firmwareDetails --sourceFirmware iot-linux --version 1.0.0 + stdout: + json: + body.sourceFirmware: r/^\d+$ + body.c8y_Firmware.name: "iot-linux" + body.c8y_Firmware.url: "https://example.com" + body.c8y_Firmware.version: "1.0.0" + + # + # Software + # + lookup software: + command: | + c8y kitchensink body_complex software --id my-app + stdout: + json: + body.id: r/^\d+$ + + lookup softwareName: + command: | + c8y kitchensink body_complex softwareName --name my-app + stdout: + json: + body.name: my-app + + lookup softwareDetails: + command: | + c8y kitchensink body_complex softwareDetails --software my-app --version "1.2.3" + stdout: + json: + body.software: r/^\d+$ + body.c8y_Software.name: my-app + body.c8y_Software.version: "1.2.3" + body.c8y_Software.url: "https://example.com/debian/my-app-1.2.3.deb" + + lookup softwareversion: + command: | + c8y kitchensink body_complex softwareversion --software my-app --version "1.2.3" + stdout: + json: + body.software: r/^\d+$ + body.version: r/^\d+$ + + lookup softwareversionName: + command: | + c8y kitchensink body_complex softwareversionName --sourceSoftware my-app --version "1.2.3" + stdout: + json: + body.sourceSoftware: r/^\d+$ + body.version: "1.2.3" diff --git a/tests/manual/extensions/example/extension_example.yaml b/tests/manual/extensions/example/extension_example.yaml new file mode 100644 index 000000000..f535b8ae2 --- /dev/null +++ b/tests/manual/extensions/example/extension_example.yaml @@ -0,0 +1,14 @@ +# yaml-language-server: $schema=https://raw.githubusercontent.com/reubenmiller/commander/feat/handle-nested-files/schema.json + +config: + env: + C8Y_SETTINGS_DEFAULTS_CACHE: true + C8Y_SETTINGS_CACHE_METHODS: GET POST PUT + C8Y_SETTINGS_DEFAULTS_CACHETTL: 100h + C8Y_SETTINGS_DEFAULTS_DRY: true + C8Y_SETTINGS_DEFAULTS_DRY_FORMAT: json + +tests: + It supports subcommands: + command: c8y kitchensink features + exit-code: 0 diff --git a/tests/manual/extensions/example/extension_presets.yaml b/tests/manual/extensions/example/extension_presets.yaml new file mode 100644 index 000000000..e37da58f0 --- /dev/null +++ b/tests/manual/extensions/example/extension_presets.yaml @@ -0,0 +1,53 @@ +# yaml-language-server: $schema=https://raw.githubusercontent.com/reubenmiller/commander/feat/handle-nested-files/schema.json + +config: + env: + C8Y_SETTINGS_DEFAULTS_CACHE: true + C8Y_SETTINGS_CACHE_METHODS: GET POST PUT + C8Y_SETTINGS_DEFAULTS_CACHETTL: 100h + C8Y_SETTINGS_DEFAULTS_DRY: true + C8Y_SETTINGS_DEFAULTS_DRY_FORMAT: json + +tests: + # query-inventory + List device collection: + command: c8y kitchensink presets query-inventory-default + exit-code: 0 + stdout: + json: + pathEncoded: /inventory/managedObjects?q=%24filter%3D+%24orderby%3Dname + + Device query with a fixed query: + command: c8y kitchensink presets query-inventory-device + exit-code: 0 + stdout: + json: + method: GET + path: /inventory/managedObjects + pathEncoded: /inventory/managedObjects?q=%24filter%3D%28type+eq+%27fixedValue%27%29+%24orderby%3Dname + + Inventory query with a fixed query: + command: c8y kitchensink presets query-inventory-mo --option myvalue + exit-code: 0 + stdout: + json: + method: GET + path: /inventory/managedObjects + pathEncoded: /inventory/managedObjects?query=%24filter%3D%28type+eq+%27company_myType%27%29+and+some.value+eq+%27myvalue%27+%24orderby%3Dname + + # get-identity + Custom identity query: + command: c8y kitchensink presets get-identity-default --name abcdef01234 + exit-code: 0 + stdout: + json: + method: GET + path: /identity/externalIds/my_Type/abcdef01234 + + Custom identity query with additional options: + command: c8y kitchensink presets get-identity-with-options --name abcdef01234 --withValues + exit-code: 0 + stdout: + json: + method: GET + pathEncoded: /identity/externalIds/my_Type/abcdef01234?withValues=true diff --git a/tests/manual/extensions/extensions_create.yaml b/tests/manual/extensions/extensions_create.yaml new file mode 100644 index 000000000..37985ddfc --- /dev/null +++ b/tests/manual/extensions/extensions_create.yaml @@ -0,0 +1,4 @@ +tests: + It can create/update/delete an extension: + command: ./manual/extensions/crud.sh + exit-code: 0 diff --git a/tests/manual/extensions/file1.txt b/tests/manual/extensions/file1.txt new file mode 100644 index 000000000..54d55bf0b --- /dev/null +++ b/tests/manual/extensions/file1.txt @@ -0,0 +1,3 @@ +one +two +three \ No newline at end of file diff --git a/tests/run-manual.sh b/tests/run-manual.sh index dc7a5fd17..f79a7bc3d 100755 --- a/tests/run-manual.sh +++ b/tests/run-manual.sh @@ -8,6 +8,9 @@ fi SCRIPT_DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" &> /dev/null && pwd )" pushd "$SCRIPT_DIR" +C8Y_SETTINGS_EXTENSIONS_DATADIR="$(pwd)/testdata" +export C8Y_SETTINGS_EXTENSIONS_DATADIR + folder= if [ $# -gt 0 ]; then folder=$1 diff --git a/tests/scripts/setup.sh b/tests/scripts/setup.sh index b895bd96b..1c17bb1ac 100755 --- a/tests/scripts/setup.sh +++ b/tests/scripts/setup.sh @@ -24,16 +24,20 @@ setup () { create_agent "agent01" create_agent "device01" create_smartgroup "my smartgroup" + create_devicegroup "My Group" create_child_device "agentParent01" "child" create_device_with_assets "agentAssetInfo01" "childAsset" create_device_with_additions "agentAdditionInfo01" "childAddition" + create_device_with_service "device02" "sshd" "systemd" "up" create_app "my-example-app" create_service_user "technician" + create_configuration "example-config" "agentConfig" "https://test.com/content/raw/app.json" create_firmware "iot-linux" create_firmware_version "iot-linux" "1.0.0" "https://example.com" + create_firmware_patch_version "iot-linux" "1.0.1" "https://example.com/patch1" create_software "my-app" create_software_version "my-app" "1.2.3" "https://example.com/debian/my-app-1.2.3.deb" @@ -89,6 +93,13 @@ create_smartgroup () { --query "name eq '*'" } +create_devicegroup () { + local name="$1" + c8y devicegroups get --id "$name" --silentStatusCodes 404 || + c8y devicegroups create \ + --name "$name" +} + create_agent () { local name="$1" c8y agents get -n --id "$name" --silentStatusCodes 404 || @@ -139,6 +150,17 @@ create_device_with_additions () { create_mo_with_name "${childNamePrefix}02" | c8y inventory update --data 'type=custominfo' | c8y devices children assign --childType addition --id "$parent" --silentStatusCodes 409 --silentExit } +create_device_with_service () { + local parentName="$1" + local serviceName="$2" + local serviceType="$3" + local serviceStatus="$4" + + parent=$(create_agent "$parentName" | c8y util show --select id --output csv ) + c8y devices services get --device "$parent" --id "$serviceName" --silentStatusCodes 404 || + c8y devices services create --device "$parent" --name "$serviceName" --serviceType "$serviceType" --status "$serviceStatus" +} + create_firmware () { local name="$1" c8y firmware get --id "$name" --silentStatusCodes 404 || @@ -153,6 +175,27 @@ create_firmware_version () { c8y firmware versions create --firmware "$name" --version "$version" --url "$url" } +create_firmware_patch_version () { + local name="$1" + local version="$2" + local dep_version="$3" + local url="$4" + c8y firmware patches get --firmware "$name" --id "$version" --silentStatusCodes 404 || + c8y firmware patches create --firmware "$name" --version "$version" --dependencyVersion "$dep_version" --url "$url" +} + +create_configuration () { + local name="$1" + local configurationType="$2" + local url="$3" + c8y configuration get --id "$name" --silentStatusCodes 404 || + c8y configuration create \ + --name "$name" \ + --description "Example config" \ + --configurationType "$configurationType" \ + --url "$url" +} + create_software () { local name="$1" c8y software get --id "$name" --silentStatusCodes 404 || diff --git a/tests/testdata/extensions/c8y-kitchensink/README.md b/tests/testdata/extensions/c8y-kitchensink/README.md new file mode 100644 index 000000000..caf7da53e --- /dev/null +++ b/tests/testdata/extensions/c8y-kitchensink/README.md @@ -0,0 +1,25 @@ +# c8y-kitchensink + +go-c8y-cli extension + +## What is included? + +**Note** + +Use ✅ or 🔲 indicates if the extension includes the given functionality or not. + + +|Type|Included|Notes| +|----|:-:|-----| +|Aliases|✅|Some useful default command like `lookup `| +|Commands|✅|Commands to manage the custom inventory managed object entities used in our IoT solution| +|Templates|✅|Some random data templates and common operations| +|Views|✅|Custom device and event views| + +## Install + +The extension can be installed using the following command. + +```sh +c8y extension install owner/c8y-kitchensink +``` diff --git a/tests/testdata/extensions/c8y-kitchensink/api/body_basic.yaml b/tests/testdata/extensions/c8y-kitchensink/api/body_basic.yaml new file mode 100644 index 000000000..56302ce30 --- /dev/null +++ b/tests/testdata/extensions/c8y-kitchensink/api/body_basic.yaml @@ -0,0 +1,211 @@ +# yaml-language-server: $schema=https://raw.githubusercontent.com/reubenmiller/go-c8y-cli/feat/extensions-manager/tools/schema/extensionCommands.json +--- +group: + name: body + description: body + +commands: + # + # Boolean + # + - name: boolean + path: inventory/managedObjects + description: Create object + method: POST + body: + - name: enable + type: boolean + description: enable + + - name: booleanDefault + path: inventory/managedObjects + description: Create object + method: POST + body: + - name: enable + type: booleanDefault + default: "false" + description: enable + + - name: active + type: booleanDefault + description: booleanDefault + default: "true" + + - name: optional_fragment + path: inventory/managedObjects + description: Create object + method: POST + body: + - name: enable + type: optional_fragment + description: enable + + # + # Date / Time + # + - name: datetime + path: inventory/managedObjects + description: Create object + method: POST + body: + - name: dateFrom + type: datetime + description: dateFrom + + - name: date + path: inventory/managedObjects + description: Create object + method: POST + body: + - name: dateFrom + type: date + description: dateFrom + + # + # Numbers + # + - name: integer + path: inventory/managedObjects + description: Create object + method: POST + body: + - name: value + type: integer + description: value + + - name: float + path: inventory/managedObjects + description: Create object + method: POST + body: + - name: value + type: float + description: value + + # + # String + # + - name: string + path: inventory/managedObjects + description: Create object + method: POST + body: + - name: value + type: string + description: value + + - name: stringStatic + path: inventory/managedObjects + description: Create object + method: POST + body: + - name: value + type: stringStatic + description: value + hidden: true + value: some value + + - name: stringArray + path: inventory/managedObjects + description: Create object + method: POST + body: + - name: value + type: string[] + description: value + + - name: stringcsvArray + path: inventory/managedObjects + description: Create object + method: POST + body: + - name: value + type: stringcsv[] + description: value + + # + # File based + # + - name: file + path: inventory/binaries + description: Upload contents of a file + method: POST + body: + - name: file + type: file + description: File + + - name: name + type: string + description: Set the name of the binary file. This will be the name of the file when it is downloaded in the UI + + - name: type + type: string + required: false + description: Custom type. If left blank, the MIME type will be detected from the file extension + + # TODO: Create a test for bodyContent.type formdata + - name: upload + path: inventory/binaries + description: Upload contents of a file + method: POST + bodyContent: + type: formdata + body: + - name: file + type: file + description: File + + - name: foo + type: string[] + description: foo + + - name: fileContents + path: inventory/binaries + description: Upload contents of a file + method: POST + body: + - name: file + type: fileContents + description: file + + - name: fileContentsAsString + path: inventory/binaries + description: Read file and upload contents as part of a json request + method: POST + body: + - name: file + type: fileContentsAsString + description: file + + - name: attachment + path: inventory/binaries + description: Upload file contents as binary + method: POST + body: + - name: file + type: attachment + description: file + + - name: binaryUploadURL + path: inventory/managedObjects + description: Add fragment via boolean flag + method: POST + body: + - name: file + type: binaryUploadURL + property: myUrl + description: url of the uploaded binary + + # + # JSON + # + - name: json + path: inventory/managedObjects + description: Create object + method: POST + body: + - name: mydata + type: json_custom + description: json diff --git a/tests/testdata/extensions/c8y-kitchensink/api/body_cumulocity.yaml b/tests/testdata/extensions/c8y-kitchensink/api/body_cumulocity.yaml new file mode 100644 index 000000000..940199eb9 --- /dev/null +++ b/tests/testdata/extensions/c8y-kitchensink/api/body_cumulocity.yaml @@ -0,0 +1,459 @@ +# yaml-language-server: $schema=https://raw.githubusercontent.com/reubenmiller/go-c8y-cli/feat/extensions-manager/tools/schema/extensionCommands.json +--- +group: + name: body_complex + description: body + +commands: + # + # Applications + # + - name: application + path: inventory/managedObjects + description: Create object + method: POST + body: + - name: id + type: application + description: application + + - name: applicationname + path: inventory/managedObjects + description: Create object + method: POST + body: + - name: name + type: applicationname + description: applicationname + + - name: hostedapplication + path: inventory/managedObjects + description: Create object + method: POST + body: + - name: id + type: hostedapplication + description: hostedapplication + + - name: microservice + path: inventory/managedObjects + description: Create object + method: POST + body: + - name: id + type: microservice + description: microservice + + - name: microservicename + path: inventory/managedObjects + description: Create object + method: POST + body: + - name: name + type: microservicename + description: microservicename + + - name: microserviceinstance + path: inventory/managedObjects + description: Create object + method: POST + body: + - name: microservice + type: microservice + description: microservice + + - name: instance + type: microserviceinstance + dependsOn: + - microservice + description: microserviceinstance + + # + # Devices / Agents / Sources + # + - name: source + path: inventory/managedObjects + description: Create object + method: POST + body: + - name: id + type: source + description: source + + - name: idArray + path: inventory/managedObjects + description: Create object + method: POST + body: + - name: id + type: id[] + description: id + + - name: agent + path: inventory/managedObjects + description: Create object + method: POST + body: + - name: id + type: agent[] + description: agent + + - name: device + path: inventory/managedObjects + description: Create object + method: POST + body: + - name: id + type: device[] + description: device + + # + # Device Groups + # + - name: devicegroup + path: inventory/managedObjects + description: Create object + method: POST + body: + - name: id + type: devicegroup[] + description: devicegroup + + - name: smartgroup + path: inventory/managedObjects + description: Create object + method: POST + body: + - name: id + type: smartgroup[] + description: smartgroup + + # + # Tenant + # + - name: tenant + path: inventory/managedObjects + description: Create object + method: POST + body: + - name: id + type: tenant + description: tenant + + - name: tenantname + path: inventory/managedObjects + description: Create object + method: POST + body: + - name: name + type: tenantname + description: tenantname + + # + # Misc + # + - name: certificate + path: inventory/managedObjects + description: Create object + method: POST + body: + - name: id + type: certificate[] + description: id + + - name: certificatefile + path: inventory/managedObjects + description: Create object + method: POST + body: + - name: file + type: certificatefile + description: file + + # + # Device management + # + - name: deviceservice + path: inventory/managedObjects + description: Create object + method: POST + body: + - name: device + type: device[] + description: device + + - name: service + type: deviceservice[] + dependsOn: + - device + description: deviceservice + + # + # Device requests + # + - name: devicerequest + path: inventory/managedObjects + description: Create object + method: POST + body: + - name: id + type: devicerequest + description: devicerequest + + - name: devicerequestArray + path: inventory/managedObjects + description: Create object + method: POST + body: + - name: id + type: devicerequest[] + description: devicerequest + + # + # Users / User groups / Roles + # + - name: role + path: inventory/managedObjects + description: Create object + method: POST + body: + - name: role + type: role[] + description: role + + - name: roleself + path: inventory/managedObjects + description: Create object + method: POST + body: + - name: roleself + type: roleself[] + description: roleself + + - name: user + path: inventory/managedObjects + description: Create object + method: POST + body: + - name: user + type: user[] + description: user + + - name: userself + path: inventory/managedObjects + description: Create object + method: POST + body: + - name: user + type: userself[] + description: user + + - name: usergroup + path: inventory/managedObjects + description: Create object + method: POST + body: + - name: group + type: usergroup[] + description: usergroup + + # -------------------------------- + # Repository + # -------------------------------- + # + # Configuration + # + - name: configuration + path: inventory/managedObjects + description: Create object + method: POST + body: + - name: id + type: configuration[] + description: configuration + + - name: configurationDetails + path: inventory/managedObjects + description: Create object + method: POST + body: + - name: id + type: configurationDetails + property: c8y_Configuration + description: configurationDetails + + # + # Device Profiles + # + - name: deviceprofile + path: inventory/managedObjects + description: Create object + method: POST + body: + - name: id + type: deviceprofile[] + description: deviceprofile + + # + # Firmware + # + - name: firmware + path: inventory/managedObjects + description: Create object + method: POST + body: + - name: id + type: firmware[] + description: firmware + + - name: firmwarename + path: inventory/managedObjects + description: Create object + method: POST + body: + - name: name + type: firmwareName + description: firmware + + - name: firmwareversion + path: inventory/managedObjects + description: Create object + method: POST + body: + - name: sourceFirmware + type: firmware[] + description: firmware + + - name: version + type: firmwareversion[] + description: firmwareversion + dependsOn: + - sourceFirmware + + - name: firmwareversionName + path: inventory/managedObjects + description: Create object + method: POST + body: + - name: firmware + type: firmware[] + description: firmware + + - name: version + type: firmwareversionName + description: firmwareversion + dependsOn: + - firmware + + - name: firmwarepatch + path: inventory/managedObjects + description: Create object + method: POST + body: + - name: sourceFirmware + type: firmware[] + description: firmware + + - name: patch + type: firmwarepatch[] + description: firmwarepatch + dependsOn: + - sourceFirmware + + - name: firmwarepatchName + path: inventory/managedObjects + description: Create object + method: POST + body: + - name: firmware + type: firmware[] + description: firmware + + - name: patch + type: firmwarepatchName + description: firmwarepatchName + dependsOn: + - firmware + + - name: firmwareDetails + path: inventory/managedObjects + description: Create object + method: POST + body: + - name: sourceFirmware + type: firmware[] + description: firmware + + - name: version + type: firmwareDetails + property: c8y_Firmware + description: firmware version + dependsOn: + - sourceFirmware + + # + # Software + # + - name: software + path: inventory/managedObjects + description: Create object + method: POST + body: + - name: id + type: software[] + description: software + + - name: softwareName + path: inventory/managedObjects + description: Create object + method: POST + body: + - name: name + type: softwareName + description: softwareName + + - name: softwareDetails + path: inventory/managedObjects + description: Create object + method: POST + body: + - name: software + type: software[] + description: software + + - name: version + type: softwareDetails + property: c8y_Software + description: version + dependsOn: + - software + + - name: softwareversion + path: inventory/managedObjects + description: Create object + method: POST + body: + - name: software + type: software[] + description: software + + - name: version + type: softwareversion[] + description: version + dependsOn: + - software + + - name: softwareversionName + path: inventory/managedObjects + description: Create object + method: POST + body: + - name: sourceSoftware + type: software[] + description: software + + - name: version + type: softwareversionName + description: version + dependsOn: + - sourceSoftware diff --git a/tests/testdata/extensions/c8y-kitchensink/api/features.yaml b/tests/testdata/extensions/c8y-kitchensink/api/features.yaml new file mode 100644 index 000000000..abd8c6e30 --- /dev/null +++ b/tests/testdata/extensions/c8y-kitchensink/api/features.yaml @@ -0,0 +1,92 @@ +# yaml-language-server: $schema=https://raw.githubusercontent.com/reubenmiller/go-c8y-cli/feat/extensions-manager/tools/schema/extensionCommands.json +--- +group: + name: features + description: Example features + +commands: + - name: completion + path: application/applications/{id} + method: GET + description: Get application + pathParameters: + - name: id + type: string + description: Application lookup + completion: + type: external + command: + - c8y + - applications + - list + - --pageSize=100 + - --type=MICROSERVICE + - --filter + - name like %s* + - --select=name,type,id + + lookup: + type: external + command: + - c8y + - applications + - list + - --pageSize=100 + - --type=MICROSERVICE + - --filter + - name like %s* + - --select=id + + - name: completion2 + path: inventory/managedObjects + method: GET + description: Get device collection + queryParameters: + - name: type + type: string + description: Device lookup with custom completions + completion: + type: external + command: + - pwsh + - -c + - c8y devices list -p 100 --query "has(enercon_Scada) and type eq 'WEC*'" --select type -o csv | sort | uniq + + - name: applications + description: application lookups + path: inventory/managedObjects + method: POST + body: + - name: id + type: microservice + description: Microservice + + - name: instance + type: microserviceinstance + description: Microservice instance + + - name: devicerequests + description: application lookups + path: inventory/managedObjects + method: POST + body: + - name: id + type: devicerequest[] + description: device request + + - name: user + description: user lookups + path: inventory/managedObjects + method: POST + body: + - name: user + type: user[] + description: user + + - name: userself + type: userself[] + description: User self + + - name: usergroup + type: usergroup[] + description: Usergroup diff --git a/tests/testdata/extensions/c8y-kitchensink/api/presets.yaml b/tests/testdata/extensions/c8y-kitchensink/api/presets.yaml new file mode 100644 index 000000000..bf8e0f118 --- /dev/null +++ b/tests/testdata/extensions/c8y-kitchensink/api/presets.yaml @@ -0,0 +1,92 @@ +# yaml-language-server: $schema=https://raw.githubusercontent.com/reubenmiller/go-c8y-cli/feat/extensions-manager/tools/schema/extensionCommands.json +--- +group: + name: presets + description: Example features + +commands: + - name: query-inventory-default + description: List device collection with defaults + preset: + type: query-inventory + + - name: query-inventory-device + description: List device collection + preset: + type: query-inventory + options: + param: q + value: (type eq 'fixedValue') + + - name: query-inventory-mo + description: List inventory collection + preset: + type: query-inventory + options: + param: query + value: (type eq 'company_myType') + extensions: + - name: option + type: string + format: some.value eq '%s' + description: option + + # child query + - name: query-inventory-children + description: List inventory collection + preset: + type: query-inventory-children + options: + param: query + value: (type eq 'company_myType') + extensions: + - name: option + type: string + format: some.value eq '%s' + description: option + pathParameters: + - name: id + property: id + type: id[] + description: test + pipeline: true + completion: + type: external + command: + - c8y + - devices + - list + - --type=WEC* + - --select=name,type,id + lookup: + type: external + command: + - c8y + - devices + - list + - --name=%s + - --type=WEC* + - --select=id + + # identity + - name: get-identity-default + description: Get identity with defaults + preset: + type: get-identity + options: + value: my_Type + + - name: get-identity-with-options + description: Get identity with defaults + preset: + type: get-identity + options: + value: my_Type + extensions: + - name: one + type: string + description: One filter + queryParameters: + - name: withValues + type: boolean + description: Custom bool diff --git a/tests/testdata/extensions/c8y-kitchensink/commands/items/list b/tests/testdata/extensions/c8y-kitchensink/commands/items/list new file mode 100755 index 000000000..cd0232e73 --- /dev/null +++ b/tests/testdata/extensions/c8y-kitchensink/commands/items/list @@ -0,0 +1,26 @@ +#!/usr/bin/env bash +set -e + +# Describe how to use your command +help() { + cat <&2 diff --git a/tests/testdata/extensions/c8y-kitchensink/extension.yaml b/tests/testdata/extensions/c8y-kitchensink/extension.yaml new file mode 100755 index 000000000..ee7ae86fd --- /dev/null +++ b/tests/testdata/extensions/c8y-kitchensink/extension.yaml @@ -0,0 +1,14 @@ +# yaml-language-server: $schema=https://raw.githubusercontent.com/reubenmiller/go-c8y-cli/feat/extensions-manager/tools/schema/extension.json +version: "v1" +aliases: + - name: lookup + description: Lookup external identity by name + command: | + identity get --name "$1" + shell: false + + - name: customCommand + description: Example description + command: | + c8y devices list --type "myType" + shell: true diff --git a/tests/testdata/extensions/c8y-kitchensink/templates/customCommand.jsonnet b/tests/testdata/extensions/c8y-kitchensink/templates/customCommand.jsonnet new file mode 100644 index 000000000..50aee2f02 --- /dev/null +++ b/tests/testdata/extensions/c8y-kitchensink/templates/customCommand.jsonnet @@ -0,0 +1,8 @@ +// Description: Create custom operation +{ + deviceId: "1234", // Dummy value (this will be overwritten when using with c8y operations create) + description: "Executing custom operation: " + $.com_CustomOperation.param1, + com_CustomOperation: { + param1: var("param1", "do_something"), + } +} \ No newline at end of file diff --git a/tests/testdata/extensions/c8y-kitchensink/views/exampleDevice.json b/tests/testdata/extensions/c8y-kitchensink/views/exampleDevice.json new file mode 100644 index 000000000..1b250ec2b --- /dev/null +++ b/tests/testdata/extensions/c8y-kitchensink/views/exampleDevice.json @@ -0,0 +1,19 @@ +{ + "$schema": "https://raw.githubusercontent.com/reubenmiller/go-c8y-cli/feat/extensions-manager/tools/schema/views.json", + "version": "v1", + "definitions": [ + { + "name": "customDevice", + "priority": 350, + "fragments": ["c8y_IsDevice"], + "columns": [ + "id", + "name", + "type", + "customProperty", + "connAlias:c8y_Availability.status", + "c8y_Connection.status" + ] + } + ] +} \ No newline at end of file diff --git a/tools/PSc8y/Private/New-ClientArgument.ps1 b/tools/PSc8y/Private/New-ClientArgument.ps1 index b20dd9e90..8f8413002 100644 --- a/tools/PSc8y/Private/New-ClientArgument.ps1 +++ b/tools/PSc8y/Private/New-ClientArgument.ps1 @@ -41,6 +41,11 @@ Function New-ClientArgument { } } + # A change in powershell handling of quoting was introduced in Powershell >= 7.3 + # This complicates a few things... + # See issue for more details: https://github.com/PowerShell/PowerShell/issues/18554 + $NeedsQuotes = ($null -eq $PSNativeCommandArgumentPassing) -or ($PSNativeCommandArgumentPassing -ne 'Standard') + foreach ($iKey in $BoundParameters.Keys) { $Value = $BoundParameters[$iKey] @@ -79,16 +84,25 @@ Function New-ClientArgument { { $Value -is [array] } { $items = Expand-Id $Value if ($items.Count -eq 1) { + $null = $c8yargs.Add("--${key}=$($items -join ',')") } elseif ($items.Count -gt 1) { - $null = $c8yargs.Add("--${key}=`"$($items -join ',')`"") + if ($NeedsQuotes) { + $null = $c8yargs.Add("--${key}=`"$($items -join ',')`"") + } else { + $null = $c8yargs.Add("--${key}=$($items -join ',')") + } } break } { $Value -match " " -and ![string]::IsNullOrWhiteSpace($Value) } { - $null = $c8yargs.Add("--${key}=`"$Value`"") + if ($NeedsQuotes) { + $null = $c8yargs.Add("--${key}=`"$Value`"") + } else { + $null = $c8yargs.Add("--${key}=$Value") + } break } diff --git a/tools/PSc8y/Public-manual/ConvertTo-JsonArgument.ps1 b/tools/PSc8y/Public-manual/ConvertTo-JsonArgument.ps1 index 3fa23c253..c609e7b6a 100644 --- a/tools/PSc8y/Public-manual/ConvertTo-JsonArgument.ps1 +++ b/tools/PSc8y/Public-manual/ConvertTo-JsonArgument.ps1 @@ -30,6 +30,11 @@ Converts the hashtable to an escaped json string [object] $Data ) + # A change in powershell handling of quoting was introduced in Powershell >= 7.3 + # This complicates a few things... + # See issue for more details: https://github.com/PowerShell/PowerShell/issues/18554 + $NeedsQuotes = ($null -eq $PSNativeCommandArgumentPassing) -or ($PSNativeCommandArgumentPassing -ne 'Standard') + if ($Data -is [string] -or $data -is [System.IO.FileSystemInfo]) { if ($Data -and (Test-Path $Data)) { # Return path as is (and let c8y binary handle it) @@ -51,11 +56,17 @@ Converts the hashtable to an escaped json string $DataObj = $Data } - # Note: replace \" with the unicode character to prevent intepretation errors on the command line - $jsonRaw = (ConvertTo-Json $DataObj -Compress -Depth 100) -replace '\\"', '\u0022' - $strArg = "{0}" -f ($jsonRaw -replace '(? limit", + "items": { + "type": "object", + "properties": { + "name": { + "type": "string", + "description": "Current property to map" + }, + "property": { + "type": "string", + "description": "Value where the name should be mapped to" + } + } + } + }, + "headerParameters": { + "type": "array", + "title": "HTTP request headers", + "description": "Set HTTP request headers from flags", + "items": { + "$ref": "#/definitions/parameterDefinition" + } + }, + "pathParameters": { + "type": "array", + "title": "HTTP request path", + "description": "Set the HTTP request path from flags", + "minItems": 1, + "items": { + "$ref": "#/definitions/parameterDefinition" + } + }, + "queryParameters": { + "type": "array", + "title": "HTTP request query parameters", + "description": "Set HTTP query parameters from flags", + "minItems": 1, + "items": { + "$ref": "#/definitions/parameterDefinition" + } + }, + "body": { + "type": "array", + "title": "HTTP request body", + "description": "Build the HTTP request body from flags", + "minItems": 1, + "items": { + "$ref": "#/definitions/parameterDefinition" + } + }, + "bodyContent": { + "type": "object", + "title": "Body content type", + "description": "Type of body when used to build the HTTP request. Only required when using non json based bodies", + "properties": { + "type": { + "type": "string", + "title": "body content type", + "description": "Body content type. Only used to identify binary contents", + "enum": [ + "binary", + "formdata" + ] + } + }, + "additionalProperties": false + }, + "bodyTemplates": { + "type": "array", + "title": "body templates", + "description": "List of static templates to apply when constructing the body of the http request. This can be used to force values, have defaults, or perform custom logic (depending on the template engine). At the moment only jsonnet is supported", + "items": { + "$ref": "#/definitions/bodyTemplate" + } + }, + "bodyRequiredKeys": { + "type": "array", + "description": "List of the required body keys. The keys will be checked after any template logic", + "items": { + "type": "string" + }, + "additionalProperties": false + }, + "bodyTemplateOptions": { + "type": "object", + "description": "Body template options", + "properties": { + "enabled": { + "type": "boolean", + "default": true, + "title": "Enable/disable template support", + "description": "Templates are enabled by default. Use this is you want to disable it. Usually it only makes sense to do for non-json based bodies" + } + }, + "additionalProperties": false + } + }, + "patternProperties": { + "^[Xx]-": {} + }, + "additionalProperties": false, + "oneOf": [ + { + "required": [ + "name", + "preset", + "description" + ] + }, + { + "required": [ + "name", + "method", + "path", + "description" + ] + } + ] + }, + "title": "List of commands", + "description": "Logically grouped commands" + } + }, + "required": [ + "group", + "commands" + ], + "patternProperties": { + "^[Xx]-": {}, + "$schema": {"type": "string"} + }, + "additionalProperties": false +} \ No newline at end of file diff --git a/tools/schema/views.json b/tools/schema/views.json new file mode 100644 index 000000000..e747f86c2 --- /dev/null +++ b/tools/schema/views.json @@ -0,0 +1,107 @@ +{ + "$schema": "http://json-schema.org/schema", + "type": "object", + "definitions": { + "view": { + "type": "object", + "properties": { + "name": { + "type": "string", + "title": "View name", + "description": "Human readable name which can be referenced by the user", + "default": "myView", + "minLength": 1 + }, + "priority": { + "type": "integer", + "title": "Priority", + "description": "If multiple matches are found, the view with the lowest number will be used", + "default": 300 + }, + "fragments": { + "type": "array", + "title": "Match by fragments", + "description": "List of fragments which must be present for the view to be activated. Regex is NOT supported", + "items": { + "type": "string" + } + }, + "type": { + "type": "string", + "title": "Match by type", + "description": "Match against the .type value. Accepts regex", + "minLength": 1, + "default": "my_type" + }, + "contentType": { + "type": "string", + "title": "Match by Content-Type header value", + "description": "Match against the response's Content-Type header. Accepts regex", + "minLength": 1, + "default": "vnd.com.nsn.cumulocity.managedObject\\+json" + }, + "requestPath": { + "type": "string", + "title": "Match by the request's path", + "description": "Match against the request's path. Accepts regex", + "minLength": 1, + "default": "" + }, + "requestMethod": { + "type": "string", + "title": "Match by the request's method", + "description": "Match against the request's method. Accepts regex", + "minLength": 1, + "default": "" + }, + "self": { + "type": "string", + "title": "Match by .self value", + "description": "Match against the response's .self property. Accepts regex", + "minLength": 1, + "default": "tenant/tenants/\\w+/trusted-certificates/" + }, + "columns": { + "type": "array", + "title": "Columns to be displayed when the view is used", + "description": "List of columns which will be used in the view. These can use the same syntax as the --select flag", + "items": { + "type": "string" + }, + "minItems": 1, + "default": ["id", "type", "**.id"] + } + }, + "required": [ + "name", + "priority", + "columns" + ], + "anyOf": [ + {"required" : ["fragments"]}, + {"required" : ["requestPath"]}, + {"required" : ["type"]}, + {"required" : ["self"]}, + {"required" : ["contentType"]} + ] + } + }, + "properties":{ + "version": { + "type": "string", + "title": "Extension version", + "description": "Extension version. Reserved for future use", + "default": "v1" + }, + "definitions": { + "type": "array", + "title": "View definitions", + "description": "List of views where each view defines when it should be used and what it should be used to display. Views can use multiple matching definitions", + "minItems": 0, + "items": { + "$ref": "#/definitions/view" + } + } + }, + "required": ["version", "definitions"] +} \ No newline at end of file