1
0
mirror of https://github.com/strongdm/comply synced 2025-04-05 06:53:27 +00:00

Compare commits

...

55 Commits

Author SHA1 Message Date
Rodolfo Campos
aab484948d
Merge pull request from darwin67/master
Update README to clarify github config attributes
2022-07-21 10:02:53 +02:00
Darwin D Wu
5371b7c8a4 Update README to clarify github config attributes 2022-07-21 00:18:38 -07:00
Rodolfo Campos
29b667f624
Merge pull request from strongdm/fix/dependency-vulnerabilities
Fix dependency vulnerabilities
2022-05-09 14:50:39 +02:00
Wallisson Rony
b386a2d227
Update mac M1 docker run instructions () 2022-05-09 09:48:43 -03:00
wallrony
62001ca96b Update README.md about running in
mac M1 ()
2022-05-06 11:25:15 -03:00
wallrony
4735c3c42d Fix comply binary outdated version () 2022-05-05 16:25:47 -03:00
wallrony
4a143af5db Fix dependency vulnerabilities ()
- Dependencies:
  - containerd/containerd
  - docker/distribuition
  - opencontainers/image-spec
2022-05-05 14:36:07 -03:00
Rodolfo Campos
e31fef1bb5
Merge pull request from alrs/theme-err
internal/theme: fix shadowed error
2022-03-24 10:12:03 +01:00
Lars Lehtonen
5b2a8e93ee
internal/theme: fix shadowed error 2022-03-24 00:23:45 -07:00
Rodolfo Campos
298220fe8e
Create SUPPORT.md 2021-12-27 12:23:10 +01:00
Rodolfo Campos
5d5773ba71
Merge pull request from rossigee/bind-address-constant
Report same address as used to bind
2021-11-05 13:07:39 +01:00
Justin McCarthy
9e9d23c60d
Slack webhook to env var 2021-11-04 22:53:34 -07:00
Justin McCarthy
08d3e750fa
increment minor for release (via Makefile) 2021-11-04 22:42:52 -07:00
Rodolfo Campos
9bcac77e52
Merge pull request from camposer/fix/infinite-build
Fix infinite build, pandoc always pulling and building files with strikethrough
2021-11-03 15:37:39 +01:00
Rodolfo Campos
4dc7f94857 Fix error when getting images without tags () 2021-11-03 12:47:41 +01:00
wallrony
c78f9007c2 Fix error handling inside pandocPandoc func ()
Other changes:
- Updated strongdm/pandoc image references to edge

Co-authored-by: vassalo <vassalo@users.noreply.github.com>
2021-11-01 15:04:27 -03:00
wallrony
3d8652f497 Merge branch 'master' of https://github.com/camposer/comply into fix/infinite-build
 Conflicts:
	fixtures/narratives/invalid-control.md
	fixtures/policies/invalid-access.md
	internal/model/fs.go
	internal/model/fs_test.go
2021-10-19 16:11:41 -03:00
wallrony
f4f46bad4d Update pandoc dockerfile and latex template and fix infinite build, fix pandoc always pull and fix strikethrough (, and ) 2021-10-19 16:05:25 -03:00
Ross Golder
c959617bf6 Fix for inconsistency between address we bind to and address we say we bind to (refs ). 2021-10-15 08:29:11 +07:00
Rodolfo Campos
ecc021985e
Merge pull request from camposer/fix/serve-handle-file-error
Fix/serve handle file error
2021-10-14 16:08:27 +02:00
Rodolfo Campos
f64aed0f60 Improve mockConfig () 2021-10-14 16:06:46 +02:00
wallrony
76fb5ec31a Add tests to procedures, policies and standards () 2021-10-13 15:04:20 -03:00
Rodolfo Campos
ad2c89ec67 Check generated documents in tests () 2021-10-12 14:46:30 +02:00
Rodolfo Campos
1c80e1ce66 Add tests () 2021-10-12 12:14:30 +02:00
vassalo
fd4aab67be fix typo 2021-10-11 10:44:36 -03:00
vassalo
bc25dae339 fix channel infinite wait () 2021-10-11 10:43:32 -03:00
vassalo
fb6d1c0bdd Merge branch 'master' of https://github.com/camposer/comply into fix/infinite-build 2021-10-08 16:16:59 -03:00
Rodolfo Campos
55fa4a7fd6
Merge pull request from camposer/fix/update-go-mod
Update go.mod and adjust pandoc and watch usability
2021-10-08 16:24:15 +02:00
Rodolfo Campos
6d2d387aa7
Merge pull request from camposer/feat/add-jira-auth-details
Add Jira auth details in README.md
2021-10-08 16:05:56 +02:00
Rodolfo Campos
f3717bea38 Improve documentation about Jira credentials () 2021-10-08 16:05:11 +02:00
wallrony
53a65f6c00 Add handling errors in read policies and narratives () 2021-10-08 10:45:41 -03:00
wallrony
373e7737ab Change loadMDMD method to return the error () 2021-10-08 10:43:08 -03:00
vassalo
b1e88ea919 handling errors when pandoc is missing () 2021-10-07 16:03:00 -03:00
wallrony
ac11b307ec Add error handling in .dm file structure () 2021-10-07 15:23:23 -03:00
wallrony
26eb39c036 Add Jira auth details in README.md () 2021-10-06 16:11:29 -03:00
wallrony
3c19f849d7 Update go.mod and adjust pandoc and watch usability () 2021-10-06 14:33:14 -03:00
Rodolfo Campos
34c0105b47
Merge pull request from adamdecaf/docs-github-config
docs: add section for github ticketing config
2021-09-29 13:28:14 +02:00
Rodolfo Campos
9209a37ccf
Merge pull request from adamdecaf/pandoc-install
cli: link to pandoc install steps
2021-09-21 12:05:24 +02:00
Rodolfo Campos
e3efe3d74f
Merge pull request from adamdecaf/2019-12-24-typos
soc2: fixup minor typos
2021-09-21 12:02:48 +02:00
Rodolfo Campos
acbd62f4df
Merge pull request from rsogithub/master
Fixed formatting typo
2021-09-21 12:00:01 +02:00
Rodolfo Campos
fffe6dc02f
Merge pull request from reicolina/reicolina-patch-1
Fixing "Publishing" paragraph in README
2021-09-21 11:39:53 +02:00
Justin McCarthy
04fd6d351b
Updated Slack invite 2021-07-12 14:56:13 -07:00
Justin McCarthy
7b75522ee0
increment patch for release (via Makefile) 2021-05-17 11:07:12 -07:00
Justin McCarthy
ccb78ddfbd
dependency fix for homebrew tap 2021-05-17 10:56:43 -07:00
Ulderico Cirello
efb59fd233 tidy go.sum 2021-03-10 12:18:46 -08:00
Phillip Markert
4370d2292c
YAML front-matter delimiter support () 2020-09-14 15:20:10 -07:00
Justin McCarthy
dbe49a09b3
increment patch for release (via Makefile) 2020-09-14 13:45:15 -07:00
Justin McCarthy
73bebe0202
go.mod: go mod vendor
Addresses 
2020-09-14 13:44:19 -07:00
Adam Shannon
022fd7b0d5 cli: link to pandoc install steps
Issue: https://github.com/strongdm/comply/issues/87
2020-05-04 10:55:54 -05:00
Adam Shannon
d6d12c3206 docs: add section for github ticketing config
Issue: https://github.com/strongdm/comply/issues/81
2019-12-26 16:05:32 -06:00
Adam Shannon
22f1657411 soc2: fixup minor typos 2019-12-24 11:04:25 -06:00
Bob Orefice
3262c130be Fixed formatting typo 2019-09-23 07:43:32 -04:00
Rei Colina
bbbd125408
Fix README typo 2019-08-15 13:34:52 -07:00
Rei Colina
9fc23274a5
Fix README typo 2019-08-15 13:34:06 -07:00
Rei Colina
1ab846e396
Fix README typo 2019-08-15 13:32:37 -07:00
1033 changed files with 106230 additions and 36084 deletions

1
.env.example Normal file

@ -0,0 +1 @@
COMPLY_USE_LOCAL_PANDOC=

5
.gitignore vendored

@ -2,4 +2,7 @@ comply
output
dist
.envrc
bindata.go
bindata.go
.idea/
.env
.vscode/

@ -1,4 +1,4 @@
FROM strongdm/pandoc:latest
FROM strongdm/pandoc:edge
# based on implementation by James Gregory <james@jagregory.com>
MAINTAINER Comply <comply@strongdm.com>
@ -7,7 +7,7 @@ RUN apt-get update -y \
&& apt-get install -y curl
ARG COMPLY_VERSION
ENV COMPLY_VERSION ${COMPLY_VERSION:-1.4.0}
ENV COMPLY_VERSION ${COMPLY_VERSION:-1.6.0}
EXPOSE 4000/tcp

@ -52,8 +52,8 @@ else
endif
docker:
cd build && docker build -t strongdm/pandoc .
docker push strongdm/pandoc
cd build && docker build -t strongdm/pandoc:edge .
docker push strongdm/pandoc:edge
cleanse:
git checkout --orphan newbranch
@ -71,6 +71,9 @@ endif
ifndef COMPLY_TAPDIR
$(error COMPLY_TAPDIR must be set to the path of the comply homebrew tap repo)
endif
ifndef COMPLY_RELEASE_WEBHOOK
$(error COMPLY_RELEASE_WEBHOOK must be set to a webhook for the release Slack channel)
endif
release: release-env dist release-deps
$(eval VERSION := $(shell git describe --tags --always --dirty="-dev"))
@ -104,11 +107,11 @@ release: release-env dist release-deps
patch-release: release-env patch release
$(eval VERSION := $(shell git describe --tags --always --dirty="-dev"))
curl -X POST --data-urlencode 'payload={"channel": "#release", "username": "release", "text": "comply $(VERSION) released", "icon_emoji": ":shipit:"}' https://hooks.slack.com/services/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
curl -X POST --data-urlencode 'payload={"channel": "#release", "username": "release", "text": "comply $(VERSION) released", "icon_emoji": ":shipit:"}' $$COMPLY_RELEASE_WEBHOOK
minor-release: release-env minor release
$(eval VERSION := $(shell git describe --tags --always --dirty="-dev"))
curl -X POST --data-urlencode 'payload={"channel": "#release", "username": "release", "text": "comply $(VERSION) released", "icon_emoji": ":shipit:"}' https://hooks.slack.com/services/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
curl -X POST --data-urlencode 'payload={"channel": "#release", "username": "release", "text": "comply $(VERSION) released", "icon_emoji": ":shipit:"}' $$COMPLY_RELEASE_WEBHOOK
docker-release:
docker build --build-arg COMPLY_VERSION=`cat VERSION` -t strongdm/comply .

@ -1,4 +1,4 @@
![Comply](https://github.com/strongdm/comply/blob/master/logo.png)
![Comply](./logo.png)
Comply is a SOC2-focused compliance automation tool:
@ -34,24 +34,27 @@ Once `comply init` is complete, just `git init` and `git push` your project to a
# Discussion
Join us in [Comply Users](https://join.slack.com/t/comply-users/shared_invite/enQtMzU3MTk5MDkxNDU4LTMwYzZkMjA4YjQ2YTM5Zjc0NTAyYWY5MDBlOGMwMzRmZTk5YzBlOTRiMTVlNGRlZjY1MTY1NDE0MjY5ZjYwNWU)
Join us in [Comply Users](https://join.slack.com/t/comply-users/shared_invite/zt-4k3f46wy-Cs1DceznNvAL~lnW9_HjIA)
# Screenshots
[Demo video](https://vimeo.com/270257486)
## Start a Project
![screencast 1](sc-1.gif)
## Build PDFs
![screencast 4](sc-4.gif)
![pdf example](pdf-example.png)
## Track Policy Coverage
![screencast 3](sc-2.gif)
## Dashboard
![screencast 2](sc-3.gif)
## Dependencies
@ -99,13 +102,43 @@ Serving content of output/ at http://127.0.0.1:4000 (ctrl-c to quit)
For Windows users, replace $PWD with the full path to your project directory
### Running in macOS M1
If you're running Comply inside Docker, or using it installed by HomeBrew, in a macOS M1, you should increase the Docker allocatable memory space to ~7 GB, but it won't run smoothly. So, we recommend to run Comply locally with pandoc binary installed via HomeBrew. For that, install the `pandoc` and `basictex` packages using the following command:
```bash
brew install pandoc basictex
```
Then when running the Comply binary -installed by HomeBrew- it will work as expected.
## Ticketing Integrations:
- Jira
- Github
- Gitlab
## Configuring Jira
## Configuration
## GitHub
Ticketing integration with GitHub can be configured with the following YAML in `comply.yml`:
```yaml
tickets:
github:
repo: <repo-name>
token: <token>
username: org or personal username
```
If you're setting up the repo in your personal account, set `username` to your username.
If you're setting up the repo in an github organization, set `username` to your org's username instead.
Also, `GITHUB_REPO`, `GITHUB_TOKEN`, and `GITHUB_USERNAME` can be used to override values from the YAML file.
### Jira
When comply creates a ticket (through `proc`, for instance), it sets the following fields.
- assignee
@ -116,12 +149,12 @@ When comply creates a ticket (through `proc`, for instance), it sets the followi
- reporter
- summary
Please make sure that the default *Create Screen* has all of those fields enabled. Additionally, make sure that there are no other required fields for the issue type you choose.
Please make sure that the default _Create Screen_ has all of those fields enabled. Additionally, make sure that there are no other required fields for the issue type you choose.
About authentication, you need to create an [API Token](https://id.atlassian.com/manage-profile/security/api-tokens) to use as a password.
## Forking and local development
> Assumes installation of golang and configuration of GOPATH in .bash_profile, .zshrc, etc
> Inspiration: http://code.openark.org/blog/development/forking-golang-repositories-on-github-and-managing-the-import-path

5
SUPPORT.md Normal file

@ -0,0 +1,5 @@
# Support
All questions/support related to Comply would be kindly addressed by the community, you can reach out to us via:
* [Issues](https://github.com/strongdm/comply/issues), or
* [Slack](https://join.slack.com/t/comply-users/shared_invite/zt-4k3f46wy-Cs1DceznNvAL~lnW9_HjIA)

@ -1 +1 @@
1.5.0
1.6.0

@ -1,29 +1,10 @@
FROM haskell:latest
FROM pandoc/ubuntu
# based on implementation by James Gregory <james@jagregory.com>
MAINTAINER Comply <comply@strongdm.com>
# install latex packages
RUN apt-get update -y \
&& apt-get install -y -o Acquire::Retries=10 --no-install-recommends \
texlive-latex-base \
texlive-xetex \
texlive-fonts-recommended \
latex-xcolor \
texlive-latex-extra \
fontconfig \
unzip \
lmodern
# will ease up the update process
# updating this env variable will trigger the automatic build of the Docker image
ENV PANDOC_VERSION "2.2.1"
# install pandoc
RUN cabal update && cabal install pandoc-${PANDOC_VERSION}
&& DEBIAN_FRONTEND=noninteractive apt-get install -y -o Acquire::Retries=10 --no-install-recommends \
texlive-latex-base \
texlive-latex-extra \
texlive-plain-generic \
lmodern
WORKDIR /source
ENTRYPOINT ["/root/.cabal/bin/pandoc"]
CMD ["--help"]

@ -1,7 +1,17 @@
package main
import "github.com/strongdm/comply/internal/cli"
import (
"fmt"
"path/filepath"
"runtime"
"github.com/joho/godotenv"
"github.com/strongdm/comply/internal/cli"
)
func main() {
_, b, _, _ := runtime.Caller(0)
basepath := filepath.Dir(b)
godotenv.Load(fmt.Sprintf("%s/.env", basepath))
cli.Main()
}

@ -20,7 +20,7 @@ Assets are built using [`comply`](https://comply.strongdm.com), which can be ins
# Publishing
The `output/` directory contains all generated assets. Links in the HTML dashboard a relative, and all dependencies are included via direct CDN references. The entire `output/` directory therefore may be uploaded to an S3 bucket or other static asset host without further modification.
The `output/` directory contains all generated assets. Links in the HTML dashboard are relative, and all dependencies are included via direct CDN references. The entire `output/` directory, therefore, may be uploaded to an S3 bucket or another static asset host without further modification.
# Dashboard Status
@ -57,4 +57,4 @@ comply build
# publish static site from output/ directory
upload.sh output/
```
```

@ -16,7 +16,7 @@ majorRevisions:
a. This policy applies to all employees and contractors. They are bound by their Employment Offer Letter or Independent Contractor Agreement to follow the Code of Conduct Policy while performing their duties. The Code of Conduct is outlined below:
#Policy
# Policy
a. *Compliance with law*

File diff suppressed because it is too large Load Diff

@ -0,0 +1,94 @@
name: Control Environment Narrative
acronym: CEN
satisfies:
TSC:
- CC2.1
- CC2.2
- CC2.3
- CC4.1
- CC4.2
- CC5.1
- CC5.2
- CC5.3
majorRevisions:
- date: Jun 1 2018
comment: Initial document
---
# Control Environment Narrative
The following provides a description of the control structure of {{.Name}}.
The intent of this description is to enumerate the logical, policy, and procedural controls that serve to monitor {{.Name}}'s application and data security. Changes uncovered by these procedures in the logical, policy, procedural, or customer environment are addressed by remediations specific to the noted change.
# Logical Controls
{{.Name}} employs several logical controls to protect confidential data and ensure normal operation of its core product.
- Mandatory data encryption at rest and in motion
- Multi-factor authentication for access to cloud infrastructure
- Activity and anomaly monitoring on production systems
- Vulnerability management program
# Policy Controls
{{.Name}} employs several policy controls to protect confidential data and ensure normal operation of its core product. These policies include, but are not limited to:
- Access Control Policy
- Encryption Policy
- Office Security Policy
- Password Policy
- Policy Training Policy
- Vendor Policy
- Workstation Policy
# Procedural Controls
{{.Name}} has numerous scheduled procedures to monitor and tune the effectiveness of ongoing security controls, and a series of event-driven procedures to respond to security-related events.
TODO: Finalize these lists
## Scheduled Security and Audit Procedures
- Review Access [quarterly]
- Review Security Logs [weekly]
- Review Cyber Risk Assessment (enumerate possible compromise scenarios) [quarterly]
- Review Data Classification [quarterly]
- Backup Testing [quarterly]
- Disaster Recovery Testing [semi-annual]
- Review Devices & Workstations [quarterly]
- Review & Clear Low-Priority Alerts [weekly]
- Apply OS Patches [monthly]
- Verify Data Disposal per Retention Policy [quarterly]
- Conduct Security Training [annual]
- Review Security Monitoring and Alerting Configuration [quarterly]
- Penetration Test [annual]
- Whitebox Security Review [annual]
- SOC2 Audit [annual]
## Event-Driven Security and Audit Procedures
- Onboard Employee
- Offboard Employee
- Investigate Security Alert
- Investigate Security Incident
# Remediations
{{.Name}} uses the outcomes of the aforementioned controls and procedures to identify shortcomings in the existing control environment. Once identified, these shortcomes are remediated by improving existing controls and procedures, and creating new controls and procedures as needed.
# Communications
{{.Name}} communicates relevant information regarding the functioning of the above controls with internal and external parties on an as-needed basis and according to statutory requirements.
## Internal
{{.Name}} communicates control outcomes, anomalies, and remediations internally using the following channels:
- Slack
- Email
- Github ticketing
## External
{{.Name}} communicates relevant control-related information to external parties including shareholders, customers, contractors, regulators, and government entities as needed according to contractual and regulatory/statutory obligation.

@ -0,0 +1,55 @@
name: Access Onboarding and Termination Policy
acronym: AOTP
satisfies:
TSC:
- CC6.1
- CC6.2
- CC6.3
majorRevisions:
- date: Jun 1 2018
comment: Initial document
---
# Purpose and Scope
a. The purpose of this policy is to define procedures to onboard and offboard users to technical infrastructure in a manner that minimizes the risk of information loss or exposure.
a. This policy applies to all technical infrastructure within the organization.
a. This policy applies to all full-time and part-time employees and contractors.
# Background
a. In order to minimize the risk of information loss or exposure (from both inside and outside the organization), the organization is reliant on the principle of least privilege. Account creation and permission levels are restricted to only the resources absolutely needed to perform each persons job duties. When a users role within the organization changes, those accounts and permission levels are changed/revoked to fit the new role and disabled when the user leaves the organization altogether.
# Policy
a. *During onboarding:*
i. Hiring Manager informs HR upon hire of a new employee.
i. HR emails IT to inform them of a new hire and their role.
i. IT creates a checklist of accounts and permission levels needed for that role.
i. The owner of each resource reviews and approves account creation and the
associated permissions.
i. IT works with the owner of each resource to set up the user.
a. *During offboarding:*
i. Hiring Manager notifies HR when an employee has been terminated.
i. HR sends a weekly email report to IT summarizing list of users terminated and instructs IT to disable their access.
i. IT terminates access within five business days from receipt of notification.
a. *When an employee changes roles within the organization:*
i. Hiring Manager will inform HR of a change in role.
i. HR and IT will follow the same steps as outlined in the onboarding and offboarding procedures.
a. *Review of accounts and permissions:*
i. Each month, IT and HR will review accounts and permission levels for accuracy.

@ -0,0 +1,39 @@
id: "workstation"
name: "Collect Workstation Details"cron: "0 0 0 15 4 *"
---
Resolve this ticket by executing the following steps:
- [ ] Send the communications below
- [ ] For any email replies, attach content to this ticket
- [ ] Validate responses are received from each
```
To: Desktop support
Subject: Annual workstation inventory
Please attach the current workstation inventory to the following ticket: [REPLACE WITH URL TO THIS TICKET]
The workstation inventory shall include the following fields:
* Serial number
* Custodian
* Full disk encryption status
* Malware protection status
```
```
To: Outsourced Call Center IT
Subject: Annual workstation inventory
As part of our ongoing compliance efforts and per our services agreement, we require a current inventory of workstations in use in the service of our account.
Please respond to this message with the current inventory.
The workstation inventory shall include the following fields:
* Serial number
* Custodian
* Full disk encryption status
* Malware protection status
```

@ -0,0 +1,245 @@
name: TSC
***CC1.1:
family: CC1
name: Integrity and Ethics
description: The entity demonstrates a commitment to integrity and ethical values
CC1.2:
family: CC1
name: Board Independence
description: The board of directors demonstrates independence from management and exercises oversight of the development and performance of internal control
CC1.3:
family: CC1
name: Organizational Structure
description: Management establishes, with board oversight, structures, reporting lines, and appropriate authorities and responsibilities in the pursuit of objectives
CC1.4:
family: CC1
name: Hiring, Training and Retention
description: The entity demonstrates a commitment to attract, develop, and retain competent individuals in alignment with objectives
CC1.5:
family: CC1
name: Individual Accountability
description: The entity holds individuals accountable for their internal control responsibilities in the pursuit of objectives.
CC2.1:
family: CC2
name: Use of Information Systems
description: The entity obtains or generates and uses relevant, quality information to support the functioning of internal control
CC2.2:
family: CC2
name: Use of Communication Systems, Internal
description: The entity internally communicates information, including objectives and responsibilities for internal control, necessary to support the functioning of internal control
CC2.3:
family: CC2
name: Use of Communication Systems, External
description: The entity communicates with external parties regarding matters affecting the functioning of internal control
CC3.1:
family: CC3
name: Objectives
description: The entity specifies objectives with sufficient clarity to enable the identification and assessment of risks relating to objectives
CC3.2:
family: CC3
name: Risk to Objectives
description: The entity identifies risks to the achievement of its objectives across the entity and analyzes risks as a basis for determining how the risks should be managed
CC3.3:
family: CC3
name: Fraud Risk to Objectives
description: The entity considers the potential for fraud in assessing risks to the achievement of objectives
CC3.4:
family: CC3
name: Impact of Changes
description: The entity identifies and assesses changes that could significantly impact the system of internal control
CC4.1:
family: CC4
name: Monitoring
description: The entity selects, develops, and performs ongoing and/or separate evaluations to ascertain whether the components of internal control are present and functioning
CC4.2:
family: CC4
name: Remediation
description: The entity evaluates and communicates internal control deficiencies in a timely manner to those parties responsible for taking corrective action, including senior management and the board of directors, as appropriate
CC5.1:
family: CC5
name: Objective Risk Mitigation
description: The entity selects and develops control activities that contribute to the mitigation of risks to the achievement of objectives to acceptable levels
CC5.2:
family: CC5
name: Technology Controls
description: The entity also selects and develops general control activities over technology to support the achievement of objectives
CC5.3:
family: CC5
name: Established Policies
description: The entity deploys control activities through policies that establish what is expected and in procedures that put policies into action
CC6.1:
family: CC6
name: Logical Access
description: The entity implements logical access security software, infrastructure, and architectures over protected information assets to protect them from security events to meet the entitys objectives
CC6.2:
family: CC6
name: User Access
description: Prior to issuing system credentials and granting system access, the entity registers and authorizes new internal and external users whose access is administered by the entity. For those users whose access is administered by the entity, user system credentials are removed when user access is no longer authorized
CC6.3:
family: CC6
name: Role-Based Access
description: The entity authorizes, modifies, or removes access to data, software, functions, and other protected information assets based on roles, responsibilities, or the system design and changes, giving consideration to the concepts of least privilege and segregation of duties, to meet the entitys objectives
CC6.4:
family: CC6
name: Physical Access
description: The entity restricts physical access to facilities and protected information assets (for example, data center facilities, back-up media storage, and other sensitive locations) to authorized personnel to meet the entitys objectives
CC6.5:
family: CC6
name: Data Disposal
description: The entity discontinues logical and physical protections over physical assets only after the ability to read or recover data and software from those assets has been diminished and is no longer required to meet the entitys objectives
CC6.6:
family: CC6
name: External Threats
description: The entity implements logical access security measures to protect against threats from sources outside its system boundaries
CC6.7:
family: CC6
name: Data Custody and Transmission
description: The entity restricts the transmission, movement, and removal of information to authorized internal and external users and processes, and protects it during transmission, movement, or removal to meet the entitys objectives
CC6.8:
family: CC6
name: Malware Detection
description: The entity implements controls to prevent or detect and act upon the introduction of unauthorized or malicious software to meet the entitys objectives
CC7.1:
family: CC7
name: Vulnerability Detection
description: To meet its objectives, the entity uses detection and monitoring procedures to identify (1) changes to configurations that result in the introduction of new vulnerabilities, and (2) susceptibilities to newly discovered vulnerabilities
CC7.2:
family: CC7
name: Anomaly Detection
description: The entity monitors system components and the operation of those components for anomalies that are indicative of malicious acts, natural disasters, and errors affecting the entitys ability to meet its objectives; anomalies are analyzed to determine whether they represent security events
CC7.3:
family: CC7
name: Security Incident Evaluation
description: The entity evaluates security events to determine whether they could or have resulted in a failure of the entity to meet its objectives (security incidents) and, if so, takes actions to prevent or address such failures
CC7.4:
family: CC7
name: Security Incident Response Plan
description: The entity responds to identified security incidents by executing a defined incident response program to understand, contain, remediate, and communicate security incidents, as appropriate
CC7.5:
family: CC7
name: Security Incident Response Execution
description: The entity identifies, develops, and implements activities to recover from identified security incidents
CC8.1:
family: CC8
name: Change Control
description: The entity authorizes, designs, develops or acquires, configures, documents, tests, approves, and implements changes to infrastructure, data, software, and procedures to meet its objectives
CC9.1:
family: CC9
name: Disruption Risk Mitigation
description: The entity identifies, selects, and develops risk mitigation activities for risks arising from potential business disruptions
CC9.2:
family: CC9
name: Vendor Risk Management
description: The entity assesses and manages risks associated with vendors and business partners
A1.1:
family: A1
name: Capacity Planning
description: The entity maintains, monitors, and evaluates current processing capacity and use of system components (infrastructure, data, and software) to manage capacity demand and to enable the implementation of additional capacity to help meet its objectives
A1.2:
family: A1
name: Backup and Recovery
description: The entity authorizes, designs, develops or acquires, implements, operates, approves, maintains, and monitors environmental protections, software, data back-up processes, and recovery infrastructure to meet its objectives
A1.3:
family: A1
name: Recovery Testing
description: The entity tests recovery plan procedures supporting system recovery to meet its objectives
C1.1:
family: C1
name: Confidential Information Identification
description: The entity identifies and maintains confidential information to meet the entitys objectives related to confidentiality
C1.2:
family: C1
name: Confidential Information Disposal
description: The entity disposes of confidential information to meet the entitys objectives related to confidentiality.
PI1.1:
family: PI1
name: Processing Integrity Monitoring
description: The entity obtains or generates, uses, and communicates relevant, quality information regarding the objectives related to processing, including definitions of data processed and product and service speci cations, to support the use of products and services
PI1.2:
family: PI1
name: Processing Integrity Accuracy
description: The entity implements policies and procedures over system inputs, including controls over completeness and accuracy, to result in products, services, and reporting to meet the entitys objectives
PI1.3:
family: PI1
name: Processing Integrity Operations
description: The entity implements policies and procedures over system processing to result in products, services, and reporting to meet the entitys objectives
PI1.4:
family: PI1
name: Processing Integrity Outputs
description: The entity implements policies and procedures to make available or deliver output completely, accurately, and timely in accordance with specifications to meet the entitys objectives
PI1.5:
family: PI1
name: Processing Integrity Backups
description: The entity implements policies and procedures to store inputs, items in processing, and outputs completely, accurately, and timely in accordance with system specifications to meet the entitys objectives
P1.1:
family: P1
name: Privacy Notification
description: The entity provides notice to data subjects about its privacy practices to meet the entitys objectives related to privacy. The notice is updated and communicated to data subjects in a timely manner for changes to the entitys privacy practices, including changes in the use of personal information, to meet the entitys objectives related to privacy
P2.1:
family: P2
name: Privacy Consent and Choice
description: The entity communicates choices available regarding the collection, use, retention, disclosure, and disposal of personal information to the data subjects and the consequences, if any, of each choice. Explicit consent for the collection, use, retention, disclosure, and disposal of personal information is obtained from data subjects or other authorized persons, if required. Such consent is obtained only for the intended purpose of the information to meet the entitys objectives related to privacy. The entitys basis for determining implicit consent for the collection, use, retention, disclosure, and disposal of personal information is documented
P3.1:
family: P3
name: Personal Information Collection
description: Personal information is collected consistent with the entitys objectives related to privacy
P3.2:
family: P3
name: Explicit Consent
description: For information requiring explicit consent, the entity communicates the need for such consent, as well as the consequences of a failure to provide consent for the request for personal information, and obtains the consent prior to the collection of the information to meet the entitys objectives related to privacy
P4.1:
family: P4
name: Proper Use of Personal Information
description: The entity limits the use of personal information to the purposes identified in the entitys objectives related to privacy
P4.2:
family: P4
name: Personal Information Retention
description: The entity retains personal information consistent with the entitys objectives related to privacy
P4.3:
family: P4
name: Personal Information Disposal
description: The entity securely disposes of personal information to meet the entitys objectives related to privacy
P5.1:
family: P5
name: Data Subject Access
description: The entity grants identified and authenticated data subjects the ability to access their stored personal information for review and, upon request, provides physical or electronic copies of that information to data subjects to meet the entitys objectives related to privacy. If access is denied, data subjects are informed of the denial and reason for such denial, as required, to meet the entitys objectives related to privacy
P5.2:
family: P5
name: Data Subject Amendment
description: The entity corrects, amends, or appends personal information based on information provided by data subjects and communicates such information to third parties, as committed or required, to meet the entitys objectives related to privacy. If a request for correction is denied, data subjects are informed of the denial and reason for such denial to meet the entitys objectives related to privacy
P6.1:
family: P6
name: Consent for Third Party Disclosure
description: The entity discloses personal information to third parties with the explicit consent of data subjects, and such consent is obtained prior to disclosure to meet the entitys objectives related to privacy
P6.2:
family: P6
name: Authorized Disclosures
description: The entity creates and retains a complete, accurate, and timely record of authorized disclosures of personal information to meet the entitys objectives related to privacy
P6.3:
family: P6
name: Unauthorized Disclosures
description: The entity creates and retains a complete, accurate, and timely record of detected or reported unauthorized disclosures (including breaches) of personal information to meet the entitys objectives related to privacy
P6.4:
family: P6
name: Appropriate Third Party Disclosure
description: The entity obtains privacy commitments from vendors and other third parties who have access to personal information to meet the entitys objectives related to privacy. The entity assesses those parties compliance on a periodic and as-needed basis and takes corrective action, if necessary
P6.5:
family: P6
name: Unauthorized Third Party Disclosure
description: The entity obtains commitments from vendors and other third parties with access to personal information to notify the entity in the event of actual or suspected unauthorized disclosures of personal information. Such notifications are reported to appropriate personnel and acted on in accordance with established incident response procedures to meet the entitys objectives related to privacy
P6.6:
family: P6
name: Notification of Unauthorized Third Party Disclosure
description: The entity provides notification of breaches and incidents to affected data subjects, regulators, and others to meet the entitys objectives related to privacy
P6.7:
family: P6
name: Accounting of Personal Information
description: The entity provides data subjects with an accounting of the personal information held and disclosure of the data subjects personal information, upon the data subjects request, to meet the entitys objectives related to privacy
P7.1:
family: P7
name: Accuracy of Personal Information
description: The entity collects and maintains accurate, up-to-date, complete, and relevant personal information to meet the entitys objectives related to privacy
P8.1:
family: P8
name: Personal Information Dispute Resolution
description: The entity implements a process for receiving, addressing, resolving, and communicating the resolution of inquiries, complaints, and disputes from data subjects and others and periodically monitors compliance to meet the entitys objectives related to privacy. Corrections and other necessary actions related to identified deficiencies are made or taken in a timely manner

84
go.mod

@ -1,49 +1,73 @@
module github.com/strongdm/comply
go 1.12
go 1.17
require (
github.com/Clever/gitsem v1.0.4
github.com/Microsoft/go-winio v0.4.14 // indirect
github.com/aktau/github-release v0.8.1
github.com/andygrunwald/go-jira v1.12.0
github.com/Clever/gitsem v1.1.0
github.com/Microsoft/go-winio v0.5.0 // indirect
github.com/aktau/github-release v0.10.0
github.com/andygrunwald/go-jira v1.14.0
github.com/containerd/containerd v1.5.10 // indirect
github.com/containous/go-bindata v1.0.0
github.com/cpuguy83/go-md2man/v2 v2.0.1 // indirect
github.com/davecgh/go-spew v1.1.1
github.com/docker/distribution v2.7.1+incompatible // indirect
github.com/docker/docker v1.13.1
github.com/docker/docker v20.10.9+incompatible
github.com/docker/go-connections v0.4.0 // indirect
github.com/docker/go-units v0.4.0 // indirect
github.com/elazarl/go-bindata-assetfs v1.0.1
github.com/fatih/color v1.9.0
github.com/fatih/structs v1.1.0 // indirect
github.com/github-release/github-release v0.8.1 // indirect
github.com/gohugoio/hugo v0.75.0
github.com/fatih/color v1.13.0
github.com/fsnotify/fsnotify v1.5.1 // indirect
github.com/github-release/github-release v0.10.0 // indirect
github.com/gohugoio/hugo v0.88.1
github.com/golang-jwt/jwt v3.2.2+incompatible // indirect
github.com/google/go-github v17.0.0+incompatible
github.com/google/go-querystring v1.1.0 // indirect
github.com/gorilla/websocket v1.4.2
github.com/hashicorp/go-retryablehttp v0.6.7 // indirect
github.com/inconshreveable/log15 v0.0.0-20200109203555-b30bc20e4fd1 // indirect
github.com/hashicorp/go-cleanhttp v0.5.2 // indirect
github.com/hashicorp/go-retryablehttp v0.7.0 // indirect
github.com/jcelliott/lumber v0.0.0-20160324203708-dd349441af25 // indirect
github.com/kevinburke/rest v0.0.0-20200429221318-0d2892b400f8 // indirect
github.com/lunixbochs/vtclean v1.0.0 // indirect
github.com/manifoldco/promptui v0.7.0
github.com/mattn/go-colorable v0.1.7 // indirect
github.com/mattn/go-runewidth v0.0.9 // indirect
github.com/joho/godotenv v1.4.0
github.com/juju/ansiterm v0.0.0-20210929141451-8b71cc96ebdc // indirect
github.com/kevinburke/rest v0.0.0-20210506044642-5611499aa33c // indirect
github.com/manifoldco/promptui v0.8.0
github.com/mattn/go-colorable v0.1.11 // indirect
github.com/mattn/go-runewidth v0.0.13 // indirect
github.com/morikuni/aec v1.0.0 // indirect
github.com/nanobox-io/golang-scribble v0.0.0-20190309225732-aa3e7c118975
github.com/olekukonko/tablewriter v0.0.4
github.com/opencontainers/go-digest v1.0.0 // indirect
github.com/olekukonko/tablewriter v0.0.5
github.com/pkg/errors v0.9.1
github.com/robfig/cron v1.2.0
github.com/skratchdot/open-golang v0.0.0-20200116055534-eef842397966
github.com/tomnomnom/linkheader v0.0.0-20180905144013-02ca5825eb80 // indirect
github.com/trivago/tgo v1.0.7 // indirect
github.com/urfave/cli v1.22.4
github.com/urfave/cli v1.22.5
github.com/voxelbrain/goptions v0.0.0-20180630082107-58cddc247ea2 // indirect
github.com/xanzy/go-gitlab v0.30.1
github.com/xanzy/go-gitlab v0.51.1
github.com/yosssi/ace v0.0.5
golang.org/x/net v0.0.0-20200904194848-62affa334b73 // indirect
golang.org/x/oauth2 v0.0.0-20200902213428-5d25da1a8d43
golang.org/x/sys v0.0.0-20200909081042-eff7692f9009 // indirect
golang.org/x/time v0.0.0-20200630173020-3af7569d3a1e // indirect
gopkg.in/blang/semver.v1 v1.1.0 // indirect
gopkg.in/yaml.v2 v2.3.0
golang.org/x/net v0.0.0-20211005001312-d4b1ae081e3b // indirect
golang.org/x/oauth2 v0.0.0-20210819190943-2bc19b11175f
golang.org/x/sys v0.0.0-20211004093028-2c5d950f24ef // indirect
golang.org/x/time v0.0.0-20210723032227-1f47c861a9ac // indirect
google.golang.org/genproto v0.0.0-20211005153810-c76a74d43a8e // indirect
google.golang.org/grpc v1.41.0 // indirect
gopkg.in/yaml.v2 v2.4.0
)
require (
github.com/chzyer/readline v0.0.0-20180603132655-2972be24d48e // indirect
github.com/docker/distribution v2.8.0+incompatible // indirect
github.com/docker/go-units v0.4.0 // indirect
github.com/dustin/go-humanize v1.0.0 // indirect
github.com/fatih/structs v1.1.0 // indirect
github.com/gogo/protobuf v1.3.2 // indirect
github.com/golang/protobuf v1.5.2 // indirect
github.com/lunixbochs/vtclean v1.0.0 // indirect
github.com/mattn/go-isatty v0.0.14 // indirect
github.com/opencontainers/go-digest v1.0.0 // indirect
github.com/opencontainers/image-spec v1.0.2 // indirect
github.com/rivo/uniseg v0.2.0 // indirect
github.com/russross/blackfriday/v2 v2.1.0 // indirect
github.com/sirupsen/logrus v1.8.1 // indirect
github.com/trivago/tgo v1.0.7 // indirect
google.golang.org/appengine v1.6.7 // indirect
google.golang.org/protobuf v1.27.1 // indirect
gopkg.in/blang/semver.v1 v1.1.0 // indirect
)

1031
go.sum

File diff suppressed because it is too large Load Diff

@ -135,12 +135,12 @@ func notifyVersion(c *cli.Context) error {
}
func pandocMustExist(c *cli.Context) error {
eitherMustExistErr := fmt.Errorf("\n\nPlease install either Docker or the pandoc package and re-run `%s`. Find OS-specific pandoc installation instructions at: [TODO]", c.Command.Name)
eitherMustExistErr := fmt.Errorf("\n\nPlease install either Docker or the pandoc package and re-run `%s`. Find OS-specific pandoc installation instructions at: https://pandoc.org/installing.html", c.Command.Name)
pandocExistErr, found, goodVersion, pdfLatex := pandocBinaryMustExist(c)
pandocBinaryExistErr, found, goodVersion, pdfLatex := pandocBinaryMustExist(c)
dockerExistErr, inPath, isRunning := dockerMustExist(c)
config.SetPandoc(pandocExistErr == nil, dockerExistErr == nil)
config.SetPandoc(pandocBinaryExistErr == nil, dockerExistErr == nil)
check := func(b bool) string {
if b {
return "✔"
@ -150,7 +150,7 @@ func pandocMustExist(c *cli.Context) error {
}
if pandocExistErr != nil && dockerExistErr != nil {
if pandocBinaryExistErr != nil && dockerExistErr != nil {
fmt.Printf(`
[%s] pandoc binary installed and in PATH
@ -165,14 +165,20 @@ func pandocMustExist(c *cli.Context) error {
}
// if we don't have pandoc, but we do have docker, execute a pull
if (pandocExistErr != nil && dockerExistErr == nil) || config.WhichPandoc() == config.UseDocker {
dockerPull(c)
if !pandocImageExists(context.Background()) && ((pandocBinaryExistErr != nil && dockerExistErr == nil) || config.WhichPandoc() == config.UseDocker) {
canPullPandoc := strings.TrimSpace(strings.ToLower(os.Getenv("COMPLY_USE_LOCAL_PANDOC"))) != "true"
if canPullPandoc {
fmt.Println("Pulling docker image")
dockerPull(c)
} else {
return fmt.Errorf("Local Pandoc not found. Please set COMPLY_USE_LOCAL_PANDOC to false")
}
}
return nil
}
func pandocBinaryMustExist(c *cli.Context) (e error, found, goodVersion, pdfLatex bool) {
var pandocBinaryMustExist = func(c *cli.Context) (e error, found, goodVersion, pdfLatex bool) {
cmd := exec.Command("pandoc", "-v")
outputRaw, err := cmd.Output()
@ -230,7 +236,7 @@ func pandocBinaryMustExist(c *cli.Context) (e error, found, goodVersion, pdfLate
return e, found, goodVersion, pdfLatex
}
func dockerMustExist(c *cli.Context) (e error, inPath, isRunning bool) {
var dockerMustExist = func(c *cli.Context) (e error, inPath, isRunning bool) {
dockerErr := fmt.Errorf("Docker must be available in order to run `%s`", c.Command.Name)
inPath = true
@ -257,7 +263,26 @@ func dockerMustExist(c *cli.Context) (e error, inPath, isRunning bool) {
return nil, inPath, isRunning
}
func dockerPull(c *cli.Context) error {
var pandocImageExists = func(ctx context.Context) bool {
cli, err := client.NewEnvClient()
if err != nil {
return false
}
options := types.ImageListOptions{All: true}
imageList, err := cli.ImageList(ctx, options)
if err != nil {
return false
}
for _, image := range imageList {
if len(image.RepoTags) > 0 && strings.Contains(image.RepoTags[0], "strongdm/pandoc:edge") {
return true
}
}
return false
}
var dockerPull = func(c *cli.Context) error {
dockerErr := fmt.Errorf("Docker must be available in order to run `%s`", c.Command.Name)
ctx := context.Background()
@ -275,7 +300,7 @@ func dockerPull(c *cli.Context) error {
select {
case <-longishPull:
fmt.Print("Pulling strongdm/pandoc:latest Docker image (this will take some time) ")
fmt.Print("Pulling strongdm/pandoc:edge Docker image (this will take some time) ")
go func() {
for {
@ -294,7 +319,7 @@ func dockerPull(c *cli.Context) error {
}
}()
r, err := cli.ImagePull(ctx, "strongdm/pandoc:latest", types.ImagePullOptions{})
r, err := cli.ImagePull(ctx, "strongdm/pandoc:edge", types.ImagePullOptions{})
if err != nil {
return dockerErr
}
@ -327,7 +352,7 @@ func cleanContainers(c *cli.Context) error {
for _, c := range containers {
// assume this container was leftover from previous aborted run
if strings.HasPrefix(c.Image, "strongdm/pandoc") {
if strings.HasPrefix(c.Image, "strongdm/pandoc:edge") {
d := time.Second * 2
err = cli.ContainerStop(ctx, c.ID, &d)
if err != nil {

189
internal/cli/app_test.go Normal file

@ -0,0 +1,189 @@
package cli
import (
"context"
"errors"
"os"
"reflect"
"testing"
"github.com/strongdm/comply/internal/util"
"github.com/urfave/cli"
)
type PandocMustExist struct{}
func TestPandocMustExist(t *testing.T) {
util.ExecuteTests(t, reflect.TypeOf(PandocMustExist{}), beforeEach, nil)
}
func beforeEach() {
util.MockConfig()
}
func (tg PandocMustExist) WhenBinaryExists(t *testing.T) {
dockerPullCalled := false
pandocBinaryMustExist = func(c *cli.Context) (e error, found, goodVersion, pdfLatex bool) {
return nil, true, true, true
}
dockerMustExist = func(c *cli.Context) (e error, inPath bool, isRunning bool) {
return errors.New("docker doesn't exist"), false, false
}
pandocImageExists = func(ctx context.Context) bool {
return false
}
dockerPull = func(c *cli.Context) error {
dockerPullCalled = true
return nil
}
pandocMustExist(&cli.Context{})
if dockerPullCalled != false {
t.Fatal("Docker was pulled")
}
}
func (tg PandocMustExist) WhenImageExists(t *testing.T) {
dockerPullCalled := false
pandocBinaryMustExist = func(c *cli.Context) (e error, found, goodVersion, pdfLatex bool) {
return errors.New("binary doesn't exist"), false, true, true
}
dockerMustExist = func(c *cli.Context) (e error, inPath bool, isRunning bool) {
return errors.New("docker doesn't exist"), false, false
}
pandocImageExists = func(ctx context.Context) bool {
return true
}
dockerPull = func(c *cli.Context) error {
dockerPullCalled = true
return nil
}
pandocMustExist(&cli.Context{})
if dockerPullCalled != false {
t.Fatal("Docker was pulled")
}
}
func (tg PandocMustExist) WhenBinaryAndImageDontExists(t *testing.T) {
dockerPullCalled := false
pandocBinaryMustExist = func(c *cli.Context) (e error, found, goodVersion, pdfLatex bool) {
return errors.New("binary doesn't exist"), false, false, false
}
dockerMustExist = func(c *cli.Context) (e error, inPath bool, isRunning bool) {
return nil, true, false
}
pandocImageExists = func(ctx context.Context) bool {
return false
}
dockerPull = func(c *cli.Context) error {
dockerPullCalled = true
return nil
}
pandocMustExist(&cli.Context{})
if dockerPullCalled != true {
t.Fatal("Docker wasn't pulled")
}
}
func (tg PandocMustExist) WhenCannotPullPandoc(t *testing.T) {
dockerPullCalled := false
pandocBinaryMustExist = func(c *cli.Context) (e error, found, goodVersion, pdfLatex bool) {
return errors.New("binary doesn't exist"), false, false, false
}
dockerMustExist = func(c *cli.Context) (e error, inPath bool, isRunning bool) {
return errors.New("docker doesn't exist"), false, false
}
pandocImageExists = func(ctx context.Context) bool {
return false
}
dockerPull = func(c *cli.Context) error {
dockerPullCalled = true
return nil
}
pandocMustExist(&cli.Context{})
if dockerPullCalled != false {
t.Fatal("Docker was pulled")
}
}
func (tg PandocMustExist) WhenMustUseLocalPandoc(t *testing.T) {
os.Setenv("COMPLY_USE_LOCAL_PANDOC", "true")
dockerPullCalled := false
pandocBinaryMustExist = func(c *cli.Context) (e error, found, goodVersion, pdfLatex bool) {
return errors.New("binary doesn't exist"), false, false, false
}
dockerMustExist = func(c *cli.Context) (e error, inPath bool, isRunning bool) {
return errors.New("docker doesn't exist"), false, false
}
pandocImageExists = func(ctx context.Context) bool {
return true
}
dockerPull = func(c *cli.Context) error {
dockerPullCalled = true
return nil
}
pandocMustExist(&cli.Context{})
if dockerPullCalled != false {
t.Fatal("Docker was pulled")
}
os.Clearenv()
}
func (tg PandocMustExist) WhenPandocDontExistsAndCannotPull(t *testing.T) {
os.Setenv("COMPLY_USE_LOCAL_PANDOC", "true")
dockerPullCalled := false
pandocBinaryMustExist = func(c *cli.Context) (e error, found, goodVersion, pdfLatex bool) {
return errors.New("binary doesn't exist"), false, false, false
}
dockerMustExist = func(c *cli.Context) (e error, inPath bool, isRunning bool) {
return nil, true, false
}
pandocImageExists = func(ctx context.Context) bool {
return false
}
dockerPull = func(c *cli.Context) error {
dockerPullCalled = true
return nil
}
pandocMustExist(&cli.Context{})
if dockerPullCalled != false {
t.Fatal("Docker was pulled")
}
os.Clearenv()
}

@ -82,7 +82,7 @@ func Exists() bool {
}
// Config is the parsed contents of ProjectRoot()/config.yml.
func Config() *Project {
var Config = func() *Project {
p := Project{}
cfgBytes, err := ioutil.ReadFile(filepath.Join(ProjectRoot(), "comply.yml"))
if err != nil {

@ -46,8 +46,8 @@ func (g *gitlabPlugin) api() *gitlab.Client {
defer g.clientMu.Unlock()
if g.client == nil {
// get go-gitlab client
gl := gitlab.NewClient(nil, g.token)
gl.SetBaseURL(g.domain)
// TODO: see if it's necessary to verify the error
gl, _ := gitlab.NewClient(g.token, gitlab.WithBaseURL(g.domain))
g.client = gl
}
return g.client
@ -160,7 +160,7 @@ func (g *gitlabPlugin) Create(ticket *model.Ticket, labels []string) error {
options := &gitlab.CreateIssueOptions{
Title: gitlab.String(ticket.Name),
Description: gitlab.String(ticket.Body),
Labels: &l,
Labels: l,
}
_, _, err := g.api().Issues.CreateIssue(g.reponame, options)
return err

@ -83,7 +83,10 @@ func ReadStandards() ([]*Standard, error) {
return nil, errors.Wrap(err, "unable to read "+f.FullPath)
}
yaml.Unmarshal(sBytes, &s)
err = yaml.Unmarshal(sBytes, &s)
if err != nil {
return nil, errors.Wrap(err, "unable to parse "+f.FullPath)
}
standards = append(standards, s)
}
@ -101,7 +104,10 @@ func ReadNarratives() ([]*Document, error) {
for _, f := range files {
n := &Document{}
mdmd := loadMDMD(f.FullPath)
mdmd, err := loadMDMD(f.FullPath)
if err != nil {
return nil, err
}
err = yaml.Unmarshal([]byte(mdmd.yaml), &n)
if err != nil {
return nil, errors.Wrap(err, "unable to parse "+f.FullPath)
@ -120,13 +126,17 @@ func ReadNarratives() ([]*Document, error) {
func ReadProcedures() ([]*Procedure, error) {
var procedures []*Procedure
files, err := path.Procedures()
if err != nil {
return nil, errors.Wrap(err, "unable to enumerate paths")
}
for _, f := range files {
p := &Procedure{}
mdmd := loadMDMD(f.FullPath)
mdmd, err := loadMDMD(f.FullPath)
if err != nil {
return nil, err
}
err = yaml.Unmarshal([]byte(mdmd.yaml), &p)
if err != nil {
return nil, errors.Wrap(err, "unable to parse "+f.FullPath)
@ -151,7 +161,10 @@ func ReadPolicies() ([]*Document, error) {
for _, f := range files {
p := &Document{}
mdmd := loadMDMD(f.FullPath)
mdmd, err := loadMDMD(f.FullPath)
if err != nil {
return nil, err
}
err = yaml.Unmarshal([]byte(mdmd.yaml), &p)
if err != nil {
return nil, errors.Wrap(err, "unable to parse "+f.FullPath)
@ -171,18 +184,20 @@ type metadataMarkdown struct {
body string
}
func loadMDMD(path string) metadataMarkdown {
func loadMDMD(path string) (*metadataMarkdown, error) {
bytes, err := ioutil.ReadFile(path)
if err != nil {
panic(err)
}
content := string(bytes)
components := strings.Split(content, "---")
if len(components) == 1 {
panic(fmt.Sprintf("Malformed metadata markdown in %s, must be of the form: YAML\\n---\\nmarkdown content", path))
if components[0] == "" && (len(components) > 1) {
components = components[1:]
}
yaml := components[0]
if len(components) == 1 {
return nil, errors.New(fmt.Sprintf("Malformed metadata markdown in %s, must be of the form: YAML\\n---\\nmarkdown content", path))
}
item := components[0]
body := strings.Join(components[1:], "---")
return metadataMarkdown{yaml, body}
return &metadataMarkdown{item, body}, nil
}

238
internal/model/fs_test.go Normal file

@ -0,0 +1,238 @@
package model
import (
"fmt"
"os"
"reflect"
"testing"
"github.com/strongdm/comply/internal/path"
"github.com/strongdm/comply/internal/util"
)
type ReadFiles struct{}
func beforeEach() {
util.MockConfig()
}
func TestReadFiles(t *testing.T) {
util.ExecuteTests(t, reflect.TypeOf(ReadFiles{}), beforeEach, nil)
}
// TestReadNarratives calls model.ReadNarratives checking for a valid return value.
func (tg ReadFiles) TestReadNarratives(t *testing.T) {
filePath := fmt.Sprintf("%s/narratives/control.md", util.GetRootPath())
fileInfo, _ := os.Lstat(filePath)
path.Narratives = func() ([]path.File, error) {
return []path.File{
{FullPath: filePath, Info: fileInfo},
}, nil
}
documents, err := ReadNarratives()
if err != nil {
t.Fatalf(`ReadNarratives() returned an error %v`, err)
}
if len(documents) != 1 {
t.Fatal(`Invalid number of documents`)
}
if documents[0].FullPath != filePath {
t.Fatalf(`Invalid document path %s`, documents[0].FullPath)
}
}
// TestReadNarrativesWhenThereAreNoNarratives calls model.ReadNarratives checking for a valid return when
// there are no narratives to process
func (tg ReadFiles) TestReadNarrativesWhenThereAreNoNarratives(t *testing.T) {
path.Narratives = func() ([]path.File, error) {
return []path.File{}, nil
}
documents, err := ReadNarratives()
if err != nil {
t.Fatalf(`ReadNarratives() returned an error %v`, err)
}
if len(documents) != 0 {
t.Fatal(`Invalid number of documents`)
}
}
// TestReadNarrativesFailsWhenInvalidNarrative calls model.ReadNarratives checking for an error return when
// there is an invalid narrative
func (tg ReadFiles) TestReadNarrativesFailsWhenInvalidNarrative(t *testing.T) {
path.Narratives = func() ([]path.File, error) {
filePath := fmt.Sprintf("%s/../fixtures/narratives/invalid-control.md", util.GetRootPath())
fileInfo, _ := os.Lstat(filePath)
return []path.File{
{FullPath: filePath, Info: fileInfo},
}, nil
}
_, err := ReadNarratives()
if err == nil {
t.Fatal(`ReadNarratives() was expected to fail`)
}
}
// TestReadProcedures calls model.ReadProcedures checking for a valid return value.
func (tg ReadFiles) TestReadProcedures(t *testing.T) {
filePath := fmt.Sprintf("%s/procedures/workstation.md", util.GetRootPath())
fileInfo, _ := os.Lstat(filePath)
path.Procedures = func() ([]path.File, error) {
return []path.File{
{FullPath: filePath, Info: fileInfo},
}, nil
}
documents, err := ReadProcedures()
if err != nil {
t.Fatalf(`ReadProcedures() returned an error %v`, err)
}
if len(documents) != 1 {
t.Fatal(`Invalid number of documents`)
}
if documents[0].FullPath != filePath {
t.Fatalf(`Invalid document path %s`, documents[0].FullPath)
}
}
// TestReadProceduresWhenThereAreNoProcedures calls model.ReadProcedures checking for a valid return when
// there are no procedures to process
func (tg ReadFiles) TestReadProceduresWhenThereAreNoProcedures(t *testing.T) {
path.Procedures = func() ([]path.File, error) {
return []path.File{}, nil
}
documents, err := ReadProcedures()
if err != nil {
t.Fatalf(`ReadProcedures() returned an error %v`, err)
}
if len(documents) != 0 {
t.Fatal(`Invalid number of documents`)
}
}
// TestReadProceduresFailsWhenInvalidProcedure calls model.ReadProcedures checking for an error return when
// there is an invalid procedure
func (tg ReadFiles) TestReadProceduresFailsWhenInvalidProcedure(t *testing.T) {
path.Procedures = func() ([]path.File, error) {
filePath := fmt.Sprintf("%s/../fixtures/procedures/invalid-workstation.md", util.GetRootPath())
fileInfo, _ := os.Lstat(filePath)
return []path.File{
{FullPath: filePath, Info: fileInfo},
}, nil
}
_, err := ReadProcedures()
if err == nil {
t.Fatal(`ReadProcedures() was expected to fail`, err)
}
}
// TestReadPolicies calls model.ReadPolicies checking for a valid return value.
func (tg ReadFiles) TestReadPolicies(t *testing.T) {
filePath := fmt.Sprintf("%s/policies/access.md", util.GetRootPath())
fileInfo, _ := os.Lstat(filePath)
path.Policies = func() ([]path.File, error) {
return []path.File{
{FullPath: filePath, Info: fileInfo},
}, nil
}
documents, err := ReadPolicies()
if err != nil {
t.Fatalf(`ReadPolicies() returned an error %v`, err)
}
if len(documents) != 1 {
t.Fatal(`Invalid number of documents`)
}
if documents[0].FullPath != filePath {
t.Fatalf(`Invalid document path %s`, documents[0].FullPath)
}
}
// TestReadPoliciesWhenThereAreNoPolicies calls model.ReadPolicies checking for a valid return when
// there are no policies to process
func (tg ReadFiles) TestReadPoliciesWhenThereAreNoPolicies(t *testing.T) {
path.Policies = func() ([]path.File, error) {
return []path.File{}, nil
}
documents, err := ReadPolicies()
if err != nil {
t.Fatalf(`ReadPolicies() returned an error %v`, err)
}
if len(documents) != 0 {
t.Fatal(`Invalid number of documents`)
}
}
// TestReadPoliciesFailsWhenInvalidPolicy calls model.ReadPolicies checking for an error return when
// there is an invalid policy
func (tg ReadFiles) TestReadPoliciesFailsWhenInvalidPolicy(t *testing.T) {
path.Policies = func() ([]path.File, error) {
filePath := fmt.Sprintf("%s/../fixtures/policies/invalid-access.md", util.GetRootPath())
fileInfo, _ := os.Lstat(filePath)
return []path.File{
{FullPath: filePath, Info: fileInfo},
}, nil
}
_, err := ReadPolicies()
if err == nil {
t.Fatal(`ReadPolicies() was expected to fail`, err)
}
}
// TestReadStandards calls model.ReadStandards checking for a valid return value.
func (tg ReadFiles) TestReadStandards(t *testing.T) {
filePath := fmt.Sprintf("%s/standards/TSC-2017.yml", util.GetRootPath())
fileInfo, _ := os.Lstat(filePath)
path.Standards = func() ([]path.File, error) {
return []path.File{
{FullPath: filePath, Info: fileInfo},
}, nil
}
documents, err := ReadStandards()
if err != nil {
t.Fatalf(`ReadStandards() returned an error %v`, err)
}
if len(documents) != 1 {
t.Fatal(`Invalid number of documents`)
}
}
// TestReadStandardsWhenThereAreNoStandards calls model.ReadStandards checking for a valid return when
// there are no standards to process
func (tg ReadFiles) TestReadStandardsWhenThereAreNoStandards(t *testing.T) {
path.Standards = func() ([]path.File, error) {
return []path.File{}, nil
}
documents, err := ReadStandards()
if err != nil {
t.Fatalf(`ReadStandards() returned an error %v`, err)
}
if len(documents) != 0 {
t.Fatal(`Invalid number of documents`)
}
}
// TestReadStandardsFailsWhenInvalidStandard calls model.ReadStandards checking for an error return when
// there is an invalid standard
func (tg ReadFiles) TestReadStandardsFailsWhenInvalidStandard(t *testing.T) {
path.Standards = func() ([]path.File, error) {
filePath := fmt.Sprintf("%s/../fixtures/standards/invalid-standard.yml", util.GetRootPath())
fileInfo, _ := os.Lstat(filePath)
return []path.File{
{FullPath: filePath, Info: fileInfo},
}, nil
}
_, err := ReadStandards()
if err == nil {
t.Fatal(`ReadStandards() was expected to fail`, err)
}
}

@ -9,36 +9,32 @@ import (
func TestMarshal(t *testing.T) {
d := Data{
Tickets: []*Ticket{
&Ticket{
{
ID: "t1",
},
},
Audits: []*Audit{
&Audit{
{
ID: "a1",
},
},
Procedures: []*Procedure{
&Procedure{
Code: "pro1",
{
ID: "pro1",
},
},
Policies: []*Policy{
&Policy{
Policies: []*Document{
{
Name: "pol1",
},
},
}
m, _ := json.Marshal(d)
encoded := string(m)
if !strings.Contains(encoded, "t1") ||
!strings.Contains(encoded, "a1") ||
!strings.Contains(encoded, "pro1") ||
!strings.Contains(encoded, "pol1") {
t.Error("identifier not found in marshalled string")
}
}

@ -16,22 +16,22 @@ type File struct {
}
// Standards lists all standard files.
func Standards() ([]File, error) {
var Standards = func() ([]File, error) {
return filesFor("standards", "yml")
}
// Narratives lists all narrative files.
func Narratives() ([]File, error) {
var Narratives = func() ([]File, error) {
return filesFor("narratives", "md")
}
// Policies lists all policy files.
func Policies() ([]File, error) {
var Policies = func() ([]File, error) {
return filesFor("policies", "md")
}
// Procedures lists all procedure files.
func Procedures() ([]File, error) {
var Procedures = func() ([]File, error) {
return filesFor("procedures", "md")
}

@ -135,9 +135,12 @@ func preprocessDoc(data *renderData, pol *model.Document, fullPath string) error
%% %s
---
header-includes: yes
head-content: "%s"
foot-content: "%s confidential %d"
header-includes: |
\usepackage{fancyhdr}
\pagestyle{fancy}
\fancyhead{}
\fancyhead[RO,RE]{%s}
\fancyfoot[LO,LE]{%s confidential %d}
---
%s

@ -18,10 +18,7 @@ var pandocArgs = []string{"-f", "markdown+smart", "--toc", "-N", "--template", "
func pandoc(outputFilename string, errOutputCh chan error) {
if config.WhichPandoc() == config.UsePandoc {
err := pandocPandoc(outputFilename)
if err != nil {
errOutputCh <- err
}
pandocPandoc(outputFilename, errOutputCh)
} else {
dockerPandoc(outputFilename, errOutputCh)
}
@ -47,9 +44,9 @@ func dockerPandoc(outputFilename string, errOutputCh chan error) {
}
resp, err := cli.ContainerCreate(ctx, &container.Config{
Image: "strongdm/pandoc",
Image: "strongdm/pandoc:edge",
Cmd: pandocCmd},
hc, nil, "")
hc, nil, nil, "")
if err != nil {
errOutputCh <- errors.Wrap(err, "unable to create Docker container")
@ -64,15 +61,20 @@ func dockerPandoc(outputFilename string, errOutputCh chan error) {
errOutputCh <- errors.Wrap(err, "unable to remove container")
return
}
errOutputCh <- nil
}()
if err := cli.ContainerStart(ctx, resp.ID, types.ContainerStartOptions{}); err != nil {
err = cli.ContainerStart(ctx, resp.ID, types.ContainerStartOptions{})
if err != nil {
errOutputCh <- errors.Wrap(err, "unable to start Docker container")
return
}
_, err = cli.ContainerWait(ctx, resp.ID)
if err != nil {
chanResult, chanErr := cli.ContainerWait(ctx, resp.ID, "not-running")
resultValue := <-chanResult
if resultValue.StatusCode != 0 {
err = <-chanErr
errOutputCh <- errors.Wrap(err, "error awaiting Docker container")
return
}
@ -90,12 +92,14 @@ func dockerPandoc(outputFilename string, errOutputCh chan error) {
}
// 🐼
func pandocPandoc(outputFilename string) error {
func pandocPandoc(outputFilename string, errOutputCh chan error) error {
cmd := exec.Command("pandoc", append(pandocArgs, fmt.Sprintf("output/%s", outputFilename), fmt.Sprintf("output/%s.md", outputFilename))...)
outputRaw, err := cmd.CombinedOutput()
if err != nil {
fmt.Println(string(outputRaw))
return errors.Wrap(err, "error calling pandoc")
errOutputCh <- errors.Wrap(err, "error calling pandoc")
} else {
errOutputCh <- nil
}
return nil
}

@ -26,6 +26,12 @@ func pdf(output string, live bool, errCh chan error, wg *sync.WaitGroup) {
}
for _, policy := range policies {
renderToFilesystem(&pdfWG, errOutputCh, data, policy, live)
err = <-errOutputCh
if err != nil {
errCh <- err
wg.Done()
return
}
}
narratives, err := model.ReadNarratives()
@ -36,6 +42,12 @@ func pdf(output string, live bool, errCh chan error, wg *sync.WaitGroup) {
for _, narrative := range narratives {
renderToFilesystem(&pdfWG, errOutputCh, data, narrative, live)
err = <-errOutputCh
if err != nil {
errCh <- err
wg.Done()
return
}
}
pdfWG.Wait()

@ -13,6 +13,8 @@ import (
"github.com/yosssi/ace"
)
const BindAddress = "0.0.0.0"
var ServePort int
var upgrader = websocket.Upgrader{
@ -94,13 +96,13 @@ func Build(output string, live bool) error {
go func() {
http.Handle("/", http.FileServer(http.Dir(filepath.Join(".", "output"))))
err := http.ListenAndServe(fmt.Sprintf("0.0.0.0:%d", ServePort), nil)
err := http.ListenAndServe(fmt.Sprintf("%s:%d", BindAddress, ServePort), nil)
if err != nil {
panic(err)
}
}()
fmt.Printf("Serving content of output/ at http://127.0.0.1:%d (ctrl-c to quit)\n", ServePort)
fmt.Printf("Serving content of output/ at http://%s:%d (ctrl-c to quit)\n", BindAddress, ServePort)
}
// PDF
wg.Add(1)

@ -8,7 +8,8 @@ import (
)
func watch(errCh chan error) {
b, err := watcher.New(300 * time.Millisecond)
// TODO: study about the poll duration
b, err := watcher.New(300 * time.Millisecond, 0, false)
if err != nil {
errCh <- err
return
@ -25,7 +26,7 @@ func watch(errCh chan error) {
go func() {
for {
select {
case e := <-b.Errors:
case e := <-b.Errors():
errCh <- e
case <-b.Events:
broadcast()

@ -29,7 +29,8 @@ func SaveTo(themeName string, replace map[string]string, saveDir string) error {
rootMdFile := string(MustAsset(name))
var w bytes.Buffer
rootMdFileTemplate, err := template.New("rootMdFile").Parse(rootMdFile)
var rootMdFileTemplate *template.Template
rootMdFileTemplate, err = template.New("rootMdFile").Parse(rootMdFile)
if err != nil {
w.WriteString(fmt.Sprintf("# Error processing template:\n\n%s\n", err.Error()))
} else {

File diff suppressed because it is too large Load Diff

@ -0,0 +1,52 @@
package util
import (
"fmt"
"io/ioutil"
"path/filepath"
"reflect"
"runtime"
"testing"
"github.com/strongdm/comply/internal/config"
"gopkg.in/yaml.v2"
)
type TestFixture func()
func ExecuteTests(t *testing.T, testGroupType reflect.Type, beforeEach TestFixture, afterEach TestFixture) {
testGroup := reflect.New(testGroupType).Elem().Interface()
for i := 0; i < testGroupType.NumMethod(); i++ {
m := testGroupType.Method(i)
t.Run(m.Name, func(t *testing.T) {
if beforeEach != nil {
beforeEach()
}
in := []reflect.Value{reflect.ValueOf(testGroup), reflect.ValueOf(t)}
m.Func.Call(in)
if afterEach != nil {
afterEach()
}
})
}
}
func MockConfig() {
config.Config = func() *config.Project {
p := config.Project{}
cfgBytes, _ := ioutil.ReadFile(filepath.Join(GetRootPath(), "comply.yml.example"))
err := yaml.Unmarshal(cfgBytes, &p)
if err != nil {
return nil
}
return &p
}
}
func GetRootPath() string {
_, fileName, _, _ := runtime.Caller(0)
fileDir := filepath.Dir(fileName)
return fmt.Sprintf("%s/../../example", fileDir)
}

@ -20,7 +20,7 @@ Assets are built using [`comply`](https://comply.strongdm.com), which can be ins
# Publishing
The `output/` directory contains all generated assets. Links in the HTML dashboard a relative, and all dependencies are included via direct CDN references. The entire `output/` directory therefore may be uploaded to an S3 bucket or other static asset host without further modification.
The `output/` directory contains all generated assets. Links in the HTML dashboard are relative, and all dependencies are included via direct CDN references. The entire `output/` directory, therefore, may be uploaded to an S3 bucket or another static asset host without further modification.
# Dashboard Status
@ -57,4 +57,4 @@ comply build
# publish static site from output/ directory
upload.sh output/
```
```

File diff suppressed because it is too large Load Diff

@ -20,7 +20,7 @@ Assets are built using [`comply`](https://comply.strongdm.com), which can be ins
# Publishing
The `output/` directory contains all generated assets. Links in the HTML dashboard a relative, and all dependencies are included via direct CDN references. The entire `output/` directory therefore may be uploaded to an S3 bucket or other static asset host without further modification.
The `output/` directory contains all generated assets. Links in the HTML dashboard are relative, and all dependencies are included via direct CDN references. The entire `output/` directory, therefore, may be uploaded to an S3 bucket or another static asset host without further modification.
# Dashboard Status
@ -57,4 +57,4 @@ comply build
# publish static site from output/ directory
upload.sh output/
```
```

@ -75,7 +75,7 @@ TODO: Finalize these lists
# Remediations
{{.Name}} uses the outcomes of the aforementioned controls and procedures to identify shortcomings in the existing control environment. Once identified, these shortcomes are remediated by improving existing controls and procedures, and creating new controls and procedures as needed.
{{.Name}} uses the outcomes of the aforementioned controls and procedures to identify shortcomings in the existing control environment. Once identified, these shortcomings are remediated by improving existing controls and procedures, and creating new controls and procedures as needed.
# Communications
@ -87,7 +87,7 @@ TODO: Finalize these lists
- Slack
- Email
- Github ticketing
- GitHub ticketing
## External

@ -66,7 +66,7 @@ a. When classifying information, the level of confidentiality is determined by:
+-------------------+------------------+---------------------------+---------------------------+
| Restricted | Restricted | Unauthorized access to | Information is available |
| | | information may cause | to a specific group of |
| | | considerable damage to | employees and authhorized |
| | | considerable damage to | employees and authorized |
| | | the business and/or | third parties. |
| | | the organization's | |
| | | reputation. | |

@ -22,7 +22,7 @@ a. This policy defines the policies and rules governing data centers and secure
# Policy
a. The following locations are classified by the organization as secure areas and are goverened by this policy:
a. The following locations are classified by the organization as secure areas and are governed by this policy:
i. [list all data center locations and secure areas under the organizations control]

@ -8,7 +8,8 @@ majorRevisions:
- date: Jun 1 2018
comment: Initial document
---
#Purpose and Scope
# Purpose and Scope
a. The purpose of this policy is to define the organizations procedures to recover Information Technology (IT) infrastructure and IT services within set deadlines in the case of a disaster or other disruptive incident. The objective of this plan is to complete the recovery of IT infrastructure and IT services within a set Recovery Time Objective (RTO).

@ -10,14 +10,14 @@ majorRevisions:
comment: Initial document
---
#Appendices
# Appendices
Appendix A: Retention Periods
# Purpose and Scope
a. This data retention policy defines the objectives and requirements for data retention within the organization.
a. This policy covers all data within the organizations custody or control, irregardless of the medium the data is stored in (electronic form, paper form, etc.) Within this policy, the medium which holds data is referred to as information, no matter what form it is in.
a. This policy covers all data within the organizations custody or control, regardless of the medium the data is stored in (electronic form, paper form, etc.) Within this policy, the medium which holds data is referred to as information, no matter what form it is in.
a. This policy applies to all users of information systems within the organization. This typically includes employees and contractors, as well as any external parties that come into contact with systems and information the organization owns or controls (hereinafter referred to as “users”). This policy must be made readily available to all users.

@ -87,13 +87,13 @@ Table 3: Description of Consequence Levels and Criteria
| Moderate | 1 | Either existing security controls have most provided an |
| | | adequate level of protection or the probability of the risk |
| | | being realized is moderate. Some minor incidents may have |
| | | occured. New incidents are possible, but not highly likely. |
| | | occurred. New incidents are possible, but not highly likely. |
+-----------------+-----------------+--------------------------------------------------------------+
| | | |
+-----------------+-----------------+--------------------------------------------------------------+
| High | 2 | Either existing security controls are not in place or |
| | | ineffective; there is a high probability of the risk being |
| | | realized. Incidents have a high likelihood of occuring in the|
| | | realized. Incidents have a high likelihood of occurring in the|
| | | future. |
+-----------------+-----------------+--------------------------------------------------------------+
| | | |

@ -154,7 +154,7 @@ C1.2:
PI1.1:
family: PI1
name: Processing Integrity Monitoring
description: The entity obtains or generates, uses, and communicates relevant, quality information regarding the objectives related to processing, including de nitions of data processed and product and service speci cations, to support the use of products and services
description: The entity obtains or generates, uses, and communicates relevant, quality information regarding the objectives related to processing, including definitions of data processed and product and service speci cations, to support the use of products and services
PI1.2:
family: PI1
name: Processing Integrity Accuracy
@ -166,11 +166,11 @@ PI1.3:
PI1.4:
family: PI1
name: Processing Integrity Outputs
description: The entity implements policies and procedures to make available or deliver output completely, accurately, and timely in accordance with speci cations to meet the entitys objectives
description: The entity implements policies and procedures to make available or deliver output completely, accurately, and timely in accordance with specifications to meet the entitys objectives
PI1.5:
family: PI1
name: Processing Integrity Backups
description: The entity implements policies and procedures to store inputs, items in processing, and outputs completely, accurately, and timely in accordance with system speci cations to meet the entitys objectives
description: The entity implements policies and procedures to store inputs, items in processing, and outputs completely, accurately, and timely in accordance with system specifications to meet the entitys objectives
P1.1:
family: P1
name: Privacy Notification

File diff suppressed because it is too large Load Diff

@ -1,24 +0,0 @@
image: bradrydzewski/go:1.3
script:
- make test
notify:
email:
recipients:
- drone@clever.com
hipchat:
room: Clever-Dev-CI
token: {{hipchat_token}}
on_started: true
on_success: true
on_failure: true
publish:
github:
branch: master
script:
- make release
artifacts:
- release
tag: v$(cat VERSION)
token: {{github_token}}
user: Clever
repo: gitsem

190
vendor/github.com/Clever/gitsem/LICENSE generated vendored Normal file

@ -0,0 +1,190 @@
Apache License
Version 2.0, January 2004
http://www.apache.org/licenses/
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
1. Definitions.
"License" shall mean the terms and conditions for use, reproduction,
and distribution as defined by Sections 1 through 9 of this document.
"Licensor" shall mean the copyright owner or entity authorized by
the copyright owner that is granting the License.
"Legal Entity" shall mean the union of the acting entity and all
other entities that control, are controlled by, or are under common
control with that entity. For the purposes of this definition,
"control" means (i) the power, direct or indirect, to cause the
direction or management of such entity, whether by contract or
otherwise, or (ii) ownership of fifty percent (50%) or more of the
outstanding shares, or (iii) beneficial ownership of such entity.
"You" (or "Your") shall mean an individual or Legal Entity
exercising permissions granted by this License.
"Source" form shall mean the preferred form for making modifications,
including but not limited to software source code, documentation
source, and configuration files.
"Object" form shall mean any form resulting from mechanical
transformation or translation of a Source form, including but
not limited to compiled object code, generated documentation,
and conversions to other media types.
"Work" shall mean the work of authorship, whether in Source or
Object form, made available under the License, as indicated by a
copyright notice that is included in or attached to the work
(an example is provided in the Appendix below).
"Derivative Works" shall mean any work, whether in Source or Object
form, that is based on (or derived from) the Work and for which the
editorial revisions, annotations, elaborations, or other modifications
represent, as a whole, an original work of authorship. For the purposes
of this License, Derivative Works shall not include works that remain
separable from, or merely link (or bind by name) to the interfaces of,
the Work and Derivative Works thereof.
"Contribution" shall mean any work of authorship, including
the original version of the Work and any modifications or additions
to that Work or Derivative Works thereof, that is intentionally
submitted to Licensor for inclusion in the Work by the copyright owner
or by an individual or Legal Entity authorized to submit on behalf of
the copyright owner. For the purposes of this definition, "submitted"
means any form of electronic, verbal, or written communication sent
to the Licensor or its representatives, including but not limited to
communication on electronic mailing lists, source code control systems,
and issue tracking systems that are managed by, or on behalf of, the
Licensor for the purpose of discussing and improving the Work, but
excluding communication that is conspicuously marked or otherwise
designated in writing by the copyright owner as "Not a Contribution."
"Contributor" shall mean Licensor and any individual or Legal Entity
on behalf of whom a Contribution has been received by Licensor and
subsequently incorporated within the Work.
2. Grant of Copyright License. Subject to the terms and conditions of
this License, each Contributor hereby grants to You a perpetual,
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
copyright license to reproduce, prepare Derivative Works of,
publicly display, publicly perform, sublicense, and distribute the
Work and such Derivative Works in Source or Object form.
3. Grant of Patent License. Subject to the terms and conditions of
this License, each Contributor hereby grants to You a perpetual,
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
(except as stated in this section) patent license to make, have made,
use, offer to sell, sell, import, and otherwise transfer the Work,
where such license applies only to those patent claims licensable
by such Contributor that are necessarily infringed by their
Contribution(s) alone or by combination of their Contribution(s)
with the Work to which such Contribution(s) was submitted. If You
institute patent litigation against any entity (including a
cross-claim or counterclaim in a lawsuit) alleging that the Work
or a Contribution incorporated within the Work constitutes direct
or contributory patent infringement, then any patent licenses
granted to You under this License for that Work shall terminate
as of the date such litigation is filed.
4. Redistribution. You may reproduce and distribute copies of the
Work or Derivative Works thereof in any medium, with or without
modifications, and in Source or Object form, provided that You
meet the following conditions:
(a) You must give any other recipients of the Work or
Derivative Works a copy of this License; and
(b) You must cause any modified files to carry prominent notices
stating that You changed the files; and
(c) You must retain, in the Source form of any Derivative Works
that You distribute, all copyright, patent, trademark, and
attribution notices from the Source form of the Work,
excluding those notices that do not pertain to any part of
the Derivative Works; and
(d) If the Work includes a "NOTICE" text file as part of its
distribution, then any Derivative Works that You distribute must
include a readable copy of the attribution notices contained
within such NOTICE file, excluding those notices that do not
pertain to any part of the Derivative Works, in at least one
of the following places: within a NOTICE text file distributed
as part of the Derivative Works; within the Source form or
documentation, if provided along with the Derivative Works; or,
within a display generated by the Derivative Works, if and
wherever such third-party notices normally appear. The contents
of the NOTICE file are for informational purposes only and
do not modify the License. You may add Your own attribution
notices within Derivative Works that You distribute, alongside
or as an addendum to the NOTICE text from the Work, provided
that such additional attribution notices cannot be construed
as modifying the License.
You may add Your own copyright statement to Your modifications and
may provide additional or different license terms and conditions
for use, reproduction, or distribution of Your modifications, or
for any such Derivative Works as a whole, provided Your use,
reproduction, and distribution of the Work otherwise complies with
the conditions stated in this License.
5. Submission of Contributions. Unless You explicitly state otherwise,
any Contribution intentionally submitted for inclusion in the Work
by You to the Licensor shall be under the terms and conditions of
this License, without any additional terms or conditions.
Notwithstanding the above, nothing herein shall supersede or modify
the terms of any separate license agreement you may have executed
with Licensor regarding such Contributions.
6. Trademarks. This License does not grant permission to use the trade
names, trademarks, service marks, or product names of the Licensor,
except as required for reasonable and customary use in describing the
origin of the Work and reproducing the content of the NOTICE file.
7. Disclaimer of Warranty. Unless required by applicable law or
agreed to in writing, Licensor provides the Work (and each
Contributor provides its Contributions) on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
implied, including, without limitation, any warranties or conditions
of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
PARTICULAR PURPOSE. You are solely responsible for determining the
appropriateness of using or redistributing the Work and assume any
risks associated with Your exercise of permissions under this License.
8. Limitation of Liability. In no event and under no legal theory,
whether in tort (including negligence), contract, or otherwise,
unless required by applicable law (such as deliberate and grossly
negligent acts) or agreed to in writing, shall any Contributor be
liable to You for damages, including any direct, indirect, special,
incidental, or consequential damages of any character arising as a
result of this License or out of the use or inability to use the
Work (including but not limited to damages for loss of goodwill,
work stoppage, computer failure or malfunction, or any and all
other commercial damages or losses), even if such Contributor
has been advised of the possibility of such damages.
9. Accepting Warranty or Additional Liability. While redistributing
the Work or Derivative Works thereof, You may choose to offer,
and charge a fee for, acceptance of support, warranty, indemnity,
or other liability obligations and/or rights consistent with this
License. However, in accepting such obligations, You may act only
on Your own behalf and on Your sole responsibility, not on behalf
of any other Contributor, and only if You agree to indemnify,
defend, and hold each Contributor harmless for any liability
incurred by, or claims asserted against, such Contributor by reason
of your accepting any such warranty or additional liability.
END OF TERMS AND CONDITIONS
Copyright 2014 Clever, Inc.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.

@ -1,8 +1,11 @@
include golang.mk
.DEFAULT_GOAL := test # override default goal set in library makefile
SHELL := /bin/bash
PKG = github.com/Clever/gitsem
PKGS = $(PKG)
VERSION := $(shell cat VERSION)
PKG := github.com/Clever/gitsem
PKGS := $(shell go list ./... | grep -v /vendor)
EXECUTABLE := gitsem
VERSION := $(shell cat VERSION)
BUILDS := \
build/$(EXECUTABLE)-v$(VERSION)-darwin-amd64 \
build/$(EXECUTABLE)-v$(VERSION)-linux-amd64 \
@ -10,32 +13,22 @@ BUILDS := \
COMPRESSED_BUILDS := $(BUILDS:%=%.tar.gz)
RELEASE_ARTIFACTS := $(COMPRESSED_BUILDS:build/%=release/%)
.PHONY: test golint
.PHONY: test golint build vendor
golint:
@go get github.com/golang/lint/golint
$(eval $(call golang-version-check,1.13))
test: $(PKGS)
$(PKGS): golint
@go get -d -t $@
@gofmt -w=true $(GOPATH)/src/$@*/**.go
ifneq ($(NOLINT),1)
@echo "LINTING..."
@PATH=$(PATH):$(GOPATH)/bin golint $(GOPATH)/src/$@*/**.go
@echo ""
endif
ifeq ($(COVERAGE),1)
@go test -cover -coverprofile=$(GOPATH)/src/$@/c.out $@ -test.v
@go tool cover -html=$(GOPATH)/src/$@/c.out
else
@echo "TESTING..."
@go test $@ -test.v
endif
$(PKGS): golang-test-all-strict-deps
$(call golang-test-all-strict,$@)
run:
@go run main.go
build:
go build -o bin/$(EXECUTABLE) $(PKG)
build/$(EXECUTABLE)-v$(VERSION)-darwin-amd64:
GOARCH=amd64 GOOS=darwin go build -o "$@/$(EXECUTABLE)"
build/$(EXECUTABLE)-v$(VERSION)-linux-amd64:
@ -52,3 +45,7 @@ release: $(RELEASE_ARTIFACTS)
clean:
rm -rf build release
install_deps:
go mod vendor

@ -32,3 +32,8 @@ In the second case, the existing version will be incremented by 1 in the specifi
### Options
- `m=%s` specifies a commit message to use when bumping the version. If %s appears, it will be replaced with the new version number.
- `tag=true` whether or not to create a tag at the version commit
## Vendoring
Please view the [dev-handbook for instructions](https://github.com/Clever/dev-handbook/blob/master/golang/godep.md).

@ -1 +1 @@
1.0.4
1.1.0

@ -7,7 +7,7 @@ import (
)
func isRepoClean() (bool, error) {
cmd := exec.Command("git", "status", "-s")
cmd := exec.Command("git", "status", "--porcelain")
result := &bytes.Buffer{}
cmd.Stdout = result
if err := cmd.Run(); err != nil {

156
vendor/github.com/Clever/gitsem/golang.mk generated vendored Normal file

@ -0,0 +1,156 @@
# This is the default Clever Golang Makefile.
# It is stored in the dev-handbook repo, github.com/Clever/dev-handbook
# Please do not alter this file directly.
GOLANG_MK_VERSION := 1.0.0
SHELL := /bin/bash
SYSTEM := $(shell uname -a | cut -d" " -f1 | tr '[:upper:]' '[:lower:]')
.PHONY: golang-test-deps golang-ensure-curl-installed
# set timezone to UTC for golang to match circle and deploys
export TZ=UTC
# go build flags for use across all commands which accept them
GO_BUILD_FLAGS := "-mod=vendor"
# if the gopath includes several directories, use only the first
GOPATH=$(shell echo $$GOPATH | cut -d: -f1)
# This block checks and confirms that the proper Go toolchain version is installed.
# It uses ^ matching in the semver sense -- you can be ahead by a minor
# version, but not a major version (patch is ignored).
# arg1: golang version
define golang-version-check
_ := $(if \
$(shell \
expr >/dev/null \
`go version | cut -d" " -f3 | cut -c3- | cut -d. -f2 | sed -E 's/beta[0-9]+//'` \
\>= `echo $(1) | cut -d. -f2` \
\& \
`go version | cut -d" " -f3 | cut -c3- | cut -d. -f1` \
= `echo $(1) | cut -d. -f1` \
&& echo 1), \
@echo "", \
$(error must be running Go version ^$(1) - you are running $(shell go version | cut -d" " -f3 | cut -c3-)))
endef
# FGT is a utility that exits with 1 whenever any stderr/stdout output is recieved.
# We pin its version since its a simple tool that does its job as-is;
# so we're defended against it breaking or changing in the future.
FGT := $(GOPATH)/bin/fgt
$(FGT):
go get github.com/GeertJohan/fgt@262f7b11eec07dc7b147c44641236f3212fee89d
golang-ensure-curl-installed:
@command -v curl >/dev/null 2>&1 || { echo >&2 "curl not installed. Please install curl."; exit 1; }
# Golint is a tool for linting Golang code for common errors.
# We pin its version because an update could add a new lint check which would make
# previously passing tests start failing without changing our code.
GOLINT := $(GOPATH)/bin/golint
$(GOLINT):
go get golang.org/x/lint/golint@738671d3881b9731cc63024d5d88cf28db875626
# golang-fmt-deps requires the FGT tool for checking output
golang-fmt-deps: $(FGT)
# golang-fmt checks that all golang files in the pkg are formatted correctly.
# arg1: pkg path
define golang-fmt
@echo "FORMATTING $(1)..."
@PKG_PATH=$$(go list -f '{{.Dir}}' $(1)); $(FGT) gofmt -l=true $${PKG_PATH}/*.go
endef
# golang-lint-deps requires the golint tool for golang linting.
golang-lint-deps: $(GOLINT)
# golang-lint calls golint on all golang files in the pkg.
# arg1: pkg path
define golang-lint
@echo "LINTING $(1)..."
@PKG_PATH=$$(go list -f '{{.Dir}}' $(1)); find $${PKG_PATH}/*.go -type f | grep -v gen_ | xargs $(GOLINT)
endef
# golang-lint-deps-strict requires the golint tool for golang linting.
golang-lint-deps-strict: $(GOLINT) $(FGT)
# golang-lint-strict calls golint on all golang files in the pkg and fails if any lint
# errors are found.
# arg1: pkg path
define golang-lint-strict
@echo "LINTING $(1)..."
@PKG_PATH=$$(go list -f '{{.Dir}}' $(1)); find $${PKG_PATH}/*.go -type f | grep -v gen_ | xargs $(FGT) $(GOLINT)
endef
# golang-test-deps is here for consistency
golang-test-deps:
# golang-test uses the Go toolchain to run all tests in the pkg.
# arg1: pkg path
define golang-test
@echo "TESTING $(1)..."
@go test $(GO_BUILD_FLAGS) -v $(1)
endef
# golang-test-strict-deps is here for consistency
golang-test-strict-deps:
# golang-test-strict uses the Go toolchain to run all tests in the pkg with the race flag
# arg1: pkg path
define golang-test-strict
@echo "TESTING $(1)..."
@go test -v $(GO_BUILD_FLAGS) -race $(1)
endef
# golang-vet-deps is here for consistency
golang-vet-deps:
# golang-vet uses the Go toolchain to vet all the pkg for common mistakes.
# arg1: pkg path
define golang-vet
@echo "VETTING $(1)..."
@go vet $(GO_BUILD_FLAGS) $(1)
endef
# golang-test-all-deps installs all dependencies needed for different test cases.
golang-test-all-deps: golang-fmt-deps golang-lint-deps golang-test-deps golang-vet-deps
# golang-test-all calls fmt, lint, vet and test on the specified pkg.
# arg1: pkg path
define golang-test-all
$(call golang-fmt,$(1))
$(call golang-lint,$(1))
$(call golang-vet,$(1))
$(call golang-test,$(1))
endef
# golang-test-all-strict-deps: installs all dependencies needed for different test cases.
golang-test-all-strict-deps: golang-fmt-deps golang-lint-deps-strict golang-test-strict-deps golang-vet-deps
# golang-test-all-strict calls fmt, lint, vet and test on the specified pkg with strict
# requirements that no errors are thrown while linting.
# arg1: pkg path
define golang-test-all-strict
$(call golang-fmt,$(1))
$(call golang-lint-strict,$(1))
$(call golang-vet,$(1))
$(call golang-test-strict,$(1))
endef
# golang-build: builds a golang binary. ensures CGO build is done during CI. This is needed to make a binary that works with a Docker alpine image.
# arg1: pkg path
# arg2: executable name
define golang-build
@echo "BUILDING..."
@if [ -z "$$CI" ]; then \
go build $(GO_BUILD_FLAGS) -o bin/$(2) $(1); \
else \
echo "-> Building CGO binary"; \
CGO_ENABLED=0 go build $(GO_BUILD_FLAGS) -installsuffix cgo -o bin/$(2) $(1); \
fi;
endef
# golang-update-makefile downloads latest version of golang.mk
golang-update-makefile:
@wget https://raw.githubusercontent.com/Clever/dev-handbook/master/make/golang-v1.mk -O /tmp/golang.mk 2>/dev/null
@if ! grep -q $(GOLANG_MK_VERSION) /tmp/golang.mk; then cp /tmp/golang.mk golang.mk && echo "golang.mk updated"; else echo "golang.mk is up-to-date"; fi

1
vendor/github.com/Microsoft/go-winio/CODEOWNERS generated vendored Normal file

@ -0,0 +1 @@
* @microsoft/containerplat

@ -1,4 +1,4 @@
# go-winio
# go-winio [![Build Status](https://github.com/microsoft/go-winio/actions/workflows/ci.yml/badge.svg)](https://github.com/microsoft/go-winio/actions/workflows/ci.yml)
This repository contains utilities for efficiently performing Win32 IO operations in
Go. Currently, this is focused on accessing named pipes and other file handles, and

@ -5,21 +5,14 @@ package winio
import (
"os"
"runtime"
"syscall"
"unsafe"
)
//sys getFileInformationByHandleEx(h syscall.Handle, class uint32, buffer *byte, size uint32) (err error) = GetFileInformationByHandleEx
//sys setFileInformationByHandle(h syscall.Handle, class uint32, buffer *byte, size uint32) (err error) = SetFileInformationByHandle
const (
fileBasicInfo = 0
fileIDInfo = 0x12
"golang.org/x/sys/windows"
)
// FileBasicInfo contains file access time and file attributes information.
type FileBasicInfo struct {
CreationTime, LastAccessTime, LastWriteTime, ChangeTime syscall.Filetime
CreationTime, LastAccessTime, LastWriteTime, ChangeTime windows.Filetime
FileAttributes uint32
pad uint32 // padding
}
@ -27,7 +20,7 @@ type FileBasicInfo struct {
// GetFileBasicInfo retrieves times and attributes for a file.
func GetFileBasicInfo(f *os.File) (*FileBasicInfo, error) {
bi := &FileBasicInfo{}
if err := getFileInformationByHandleEx(syscall.Handle(f.Fd()), fileBasicInfo, (*byte)(unsafe.Pointer(bi)), uint32(unsafe.Sizeof(*bi))); err != nil {
if err := windows.GetFileInformationByHandleEx(windows.Handle(f.Fd()), windows.FileBasicInfo, (*byte)(unsafe.Pointer(bi)), uint32(unsafe.Sizeof(*bi))); err != nil {
return nil, &os.PathError{Op: "GetFileInformationByHandleEx", Path: f.Name(), Err: err}
}
runtime.KeepAlive(f)
@ -36,13 +29,32 @@ func GetFileBasicInfo(f *os.File) (*FileBasicInfo, error) {
// SetFileBasicInfo sets times and attributes for a file.
func SetFileBasicInfo(f *os.File, bi *FileBasicInfo) error {
if err := setFileInformationByHandle(syscall.Handle(f.Fd()), fileBasicInfo, (*byte)(unsafe.Pointer(bi)), uint32(unsafe.Sizeof(*bi))); err != nil {
if err := windows.SetFileInformationByHandle(windows.Handle(f.Fd()), windows.FileBasicInfo, (*byte)(unsafe.Pointer(bi)), uint32(unsafe.Sizeof(*bi))); err != nil {
return &os.PathError{Op: "SetFileInformationByHandle", Path: f.Name(), Err: err}
}
runtime.KeepAlive(f)
return nil
}
// FileStandardInfo contains extended information for the file.
// FILE_STANDARD_INFO in WinBase.h
// https://docs.microsoft.com/en-us/windows/win32/api/winbase/ns-winbase-file_standard_info
type FileStandardInfo struct {
AllocationSize, EndOfFile int64
NumberOfLinks uint32
DeletePending, Directory bool
}
// GetFileStandardInfo retrieves ended information for the file.
func GetFileStandardInfo(f *os.File) (*FileStandardInfo, error) {
si := &FileStandardInfo{}
if err := windows.GetFileInformationByHandleEx(windows.Handle(f.Fd()), windows.FileStandardInfo, (*byte)(unsafe.Pointer(si)), uint32(unsafe.Sizeof(*si))); err != nil {
return nil, &os.PathError{Op: "GetFileInformationByHandleEx", Path: f.Name(), Err: err}
}
runtime.KeepAlive(f)
return si, nil
}
// FileIDInfo contains the volume serial number and file ID for a file. This pair should be
// unique on a system.
type FileIDInfo struct {
@ -53,7 +65,7 @@ type FileIDInfo struct {
// GetFileID retrieves the unique (volume, file ID) pair for a file.
func GetFileID(f *os.File) (*FileIDInfo, error) {
fileID := &FileIDInfo{}
if err := getFileInformationByHandleEx(syscall.Handle(f.Fd()), fileIDInfo, (*byte)(unsafe.Pointer(fileID)), uint32(unsafe.Sizeof(*fileID))); err != nil {
if err := windows.GetFileInformationByHandleEx(windows.Handle(f.Fd()), windows.FileIdInfo, (*byte)(unsafe.Pointer(fileID)), uint32(unsafe.Sizeof(*fileID))); err != nil {
return nil, &os.PathError{Op: "GetFileInformationByHandleEx", Path: f.Name(), Err: err}
}
runtime.KeepAlive(f)

@ -1,9 +0,0 @@
module github.com/Microsoft/go-winio
go 1.12
require (
github.com/pkg/errors v0.8.1
github.com/sirupsen/logrus v1.4.1
golang.org/x/sys v0.0.0-20190507160741-ecd444e8653b
)

@ -1,16 +0,0 @@
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=
github.com/konsorten/go-windows-terminal-sequences v1.0.1 h1:mweAR1A6xJ3oS2pRaGiHgQ4OO8tzTaLawm8vnODuwDk=
github.com/konsorten/go-windows-terminal-sequences v1.0.1/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ=
github.com/pkg/errors v0.8.1 h1:iURUrRGxPUNPdy5/HRSm+Yj6okJ6UtLINN0Q9M4+h3I=
github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/sirupsen/logrus v1.4.1 h1:GL2rEmy6nsikmW0r8opw9JIRScdMF5hA8cOYLH7In1k=
github.com/sirupsen/logrus v1.4.1/go.mod h1:ni0Sbl8bgC9z8RoU9G6nDWqqs/fq4eDPysMBDgk/93Q=
github.com/stretchr/objx v0.1.1/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/testify v1.2.2 h1:bSDNvY7ZPG5RlJ8otE/7V6gMiyenm9RtJ7IUVIAoJ1w=
github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs=
golang.org/x/sys v0.0.0-20180905080454-ebe1bf3edb33/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20190507160741-ecd444e8653b h1:ag/x1USPSsqHud38I9BAC88qdNLDHHtQ4mlgQIZPPNA=
golang.org/x/sys v0.0.0-20190507160741-ecd444e8653b/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=

@ -1,3 +1,5 @@
// +build windows
package winio
import (

@ -182,13 +182,14 @@ func (s pipeAddress) String() string {
}
// tryDialPipe attempts to dial the pipe at `path` until `ctx` cancellation or timeout.
func tryDialPipe(ctx context.Context, path *string) (syscall.Handle, error) {
func tryDialPipe(ctx context.Context, path *string, access uint32) (syscall.Handle, error) {
for {
select {
case <-ctx.Done():
return syscall.Handle(0), ctx.Err()
default:
h, err := createFile(*path, syscall.GENERIC_READ|syscall.GENERIC_WRITE, 0, nil, syscall.OPEN_EXISTING, syscall.FILE_FLAG_OVERLAPPED|cSECURITY_SQOS_PRESENT|cSECURITY_ANONYMOUS, 0)
h, err := createFile(*path, access, 0, nil, syscall.OPEN_EXISTING, syscall.FILE_FLAG_OVERLAPPED|cSECURITY_SQOS_PRESENT|cSECURITY_ANONYMOUS, 0)
if err == nil {
return h, nil
}
@ -197,7 +198,7 @@ func tryDialPipe(ctx context.Context, path *string) (syscall.Handle, error) {
}
// Wait 10 msec and try again. This is a rather simplistic
// view, as we always try each 10 milliseconds.
time.Sleep(time.Millisecond * 10)
time.Sleep(10 * time.Millisecond)
}
}
}
@ -210,7 +211,7 @@ func DialPipe(path string, timeout *time.Duration) (net.Conn, error) {
if timeout != nil {
absTimeout = time.Now().Add(*timeout)
} else {
absTimeout = time.Now().Add(time.Second * 2)
absTimeout = time.Now().Add(2 * time.Second)
}
ctx, _ := context.WithDeadline(context.Background(), absTimeout)
conn, err := DialPipeContext(ctx, path)
@ -223,9 +224,15 @@ func DialPipe(path string, timeout *time.Duration) (net.Conn, error) {
// DialPipeContext attempts to connect to a named pipe by `path` until `ctx`
// cancellation or timeout.
func DialPipeContext(ctx context.Context, path string) (net.Conn, error) {
return DialPipeAccess(ctx, path, syscall.GENERIC_READ|syscall.GENERIC_WRITE)
}
// DialPipeAccess attempts to connect to a named pipe by `path` with `access` until `ctx`
// cancellation or timeout.
func DialPipeAccess(ctx context.Context, path string, access uint32) (net.Conn, error) {
var err error
var h syscall.Handle
h, err = tryDialPipe(ctx, &path)
h, err = tryDialPipe(ctx, &path, access)
if err != nil {
return nil, err
}
@ -422,10 +429,10 @@ type PipeConfig struct {
// when the pipe is in message mode.
MessageMode bool
// InputBufferSize specifies the size the input buffer, in bytes.
// InputBufferSize specifies the size of the input buffer, in bytes.
InputBufferSize int32
// OutputBufferSize specifies the size the input buffer, in bytes.
// OutputBufferSize specifies the size of the output buffer, in bytes.
OutputBufferSize int32
}

@ -1,3 +1,5 @@
// +build windows
// Package guid provides a GUID type. The backing structure for a GUID is
// identical to that used by the golang.org/x/sys/windows GUID type.
// There are two main binary encodings used for a GUID, the big-endian encoding,

@ -28,8 +28,9 @@ const (
ERROR_NOT_ALL_ASSIGNED syscall.Errno = 1300
SeBackupPrivilege = "SeBackupPrivilege"
SeRestorePrivilege = "SeRestorePrivilege"
SeBackupPrivilege = "SeBackupPrivilege"
SeRestorePrivilege = "SeRestorePrivilege"
SeSecurityPrivilege = "SeSecurityPrivilege"
)
const (

@ -1,3 +1,3 @@
package winio
//go:generate go run $GOROOT/src/syscall/mksyscall_windows.go -output zsyscall_windows.go file.go pipe.go sd.go fileinfo.go privilege.go backup.go hvsock.go
//go:generate go run golang.org/x/sys/windows/mkwinsyscall -output zsyscall_windows.go file.go pipe.go sd.go fileinfo.go privilege.go backup.go hvsock.go

File diff suppressed because it is too large Load Diff

@ -10,44 +10,15 @@
version = "v1.0.0"
[[projects]]
digest = "1:586ea76dbd0374d6fb649a91d70d652b7fe0ccffb8910a77468e7702e7901f3d"
name = "github.com/go-stack/stack"
packages = ["."]
pruneopts = "UT"
revision = "2fee6af1a9795aafbe0253a0cfbdf668e1fb8a9a"
version = "v1.8.0"
[[projects]]
digest = "1:5c56664d98f37f0ee54bf572b0b189a3910c34c31052fc7d58b282c449b079fb"
name = "github.com/inconshreveable/log15"
packages = ["."]
pruneopts = "UT"
revision = "b30bc20e4fd12cec79a9aae62e91cfcf458bd253"
version = "v2.15"
[[projects]]
digest = "1:de0adde670b2119824a1252b61a0e989669f8b24af874e399bec0e0538b2f928"
digest = "1:7462b19f7e4adb24e966b77213c693dad09c04d58ac8d29ddf449ff1b1c91d11"
name = "github.com/kevinburke/rest"
packages = ["."]
packages = [
"restclient",
"resterror",
]
pruneopts = "UT"
revision = "0d2892b400f81cdfb979e2f718e6070fae17a507"
version = "2.2"
[[projects]]
digest = "1:0109cf4321a15313ec895f42e723e1f76121c6975ea006abfa20012272ec0937"
name = "github.com/mattn/go-colorable"
packages = ["."]
pruneopts = "UT"
revision = "68e95eba382c972aafde02ead2cd2426a8a92480"
version = "v0.1.6"
[[projects]]
digest = "1:0c58d31abe2a2ccb429c559b6292e7df89dcda675456fecc282fa90aa08273eb"
name = "github.com/mattn/go-isatty"
packages = ["."]
pruneopts = "UT"
revision = "7b513a986450394f7bbf1476909911b3aa3a55ce"
version = "v0.0.12"
revision = "22cd0577e450f2fa21313f7eaf42b41a178291c1"
version = "2.5"
[[projects]]
branch = "master"
@ -65,23 +36,12 @@
revision = "26cb8b04692384f4dc269de3b5fcf3e2ef78573e"
version = "2.5.11"
[[projects]]
branch = "master"
digest = "1:8cab10971112233c82c83683a517378038eba1c20e71b29c592b73fa212437b3"
name = "golang.org/x/sys"
packages = [
"internal/unsafeheader",
"unix",
]
pruneopts = "UT"
revision = "bc7a7d42d5c30f4d0fe808715c002826ce2c624e"
[solve-meta]
analyzer-name = "dep"
analyzer-version = 1
input-imports = [
"github.com/dustin/go-humanize",
"github.com/kevinburke/rest",
"github.com/kevinburke/rest/restclient",
"github.com/tomnomnom/linkheader",
"github.com/voxelbrain/goptions",
]

@ -9,7 +9,7 @@
[[constraint]]
name = "github.com/kevinburke/rest"
version = "2.2.0"
version = "2.5.0"
[[constraint]]
name = "github.com/tomnomnom/linkheader"

@ -91,6 +91,9 @@ clean:
rm $(EXECUTABLE) || true
rm -rf bin/
lint:
go vet ./...
test:
go test ./...

@ -101,6 +101,13 @@ $ github-release delete \
--tag v0.1.0
```
Errata
======
The `release` command does not have an `--auth-user` flag because in practice,
Github ignores the `--auth-user` flag when validating releases. The only thing
that matters is passing a token that has permission to create the release.
GitHub Enterprise Support
=========================
You can point to a different GitHub API endpoint via the environment variable ```GITHUB_API```:

@ -354,10 +354,17 @@ func releasecmd(opt Options) error {
}
reader := bytes.NewReader(payload)
URL := nvls(EnvApiEndpoint, github.DefaultBaseURL) + fmt.Sprintf("/repos/%s/%s/releases", user, repo)
resp, err := github.DoAuthRequest("POST", URL, "application/json", token, nil, reader)
// NB: Github appears to ignore the user here - the only thing that seems to
// matter is that the token is valid.
client := github.NewClient(user, token, nil)
client.SetBaseURL(EnvApiEndpoint)
req, err := client.NewRequest("POST", fmt.Sprintf("/repos/%s/%s/releases", user, repo), reader)
if err != nil {
return fmt.Errorf("while submitting %v, %v", string(payload), err)
return fmt.Errorf("while submitting %v: %w", string(payload), err)
}
resp, err := client.Do(req)
if err != nil {
return fmt.Errorf("while submitting %v: %w", string(payload), err)
}
defer resp.Body.Close()

@ -1,20 +0,0 @@
language: go
sudo: false
go:
- "1.9.x"
- "1.10.x"
- "1.11.x"
- "1.12.x"
- "1.13.x"
before_install:
- go get -t ./...
matrix:
allow_failures:
- go: 1.13.x
script:
- GOMAXPROCS=4 GORACE="halt_on_error=1" go test -race -v ./...

@ -2,6 +2,32 @@
All notable changes to this project will be documented in this file. See [standard-version](https://github.com/conventional-changelog/standard-version) for commit guidelines.
## [1.13.0](https://github.com/andygrunwald/go-jira/compare/v1.11.1...v1.13.0) (2020-10-25)
### Features
* add AddRemoteLink method ([f200e15](https://github.com/andygrunwald/go-jira/commit/f200e158b997a303db081cbbc5a9d8ad5d89566d)), closes [/developer.atlassian.com/cloud/jira/platform/rest/v2/#api-rest-api-2](https://github.com/andygrunwald//developer.atlassian.com/cloud/jira/platform/rest/v2//issues/api-rest-api-2)
* Add Names support on Issue struct ([#278](https://github.com/andygrunwald/go-jira/issues/278)) ([1fc10e0](https://github.com/andygrunwald/go-jira/commit/1fc10e0606784f745673ccc4d8d706c36f385a7a))
* Extend Makefile for more source code quality targets ([5e52236](https://github.com/andygrunwald/go-jira/commit/5e5223631a29d10a13e598318a6abe47384e2982))
* **context:** Add support for context package ([e1f4265](https://github.com/andygrunwald/go-jira/commit/e1f4265e2b467b938fe0c095caf6d36f3136d2ff))
* **issues:** Add GetEditMeta on issue ([a783764](https://github.com/andygrunwald/go-jira/commit/a783764b52dc890773658ddd0483a9d0393e385d)), closes [/docs.atlassian.com/DAC/rest/jira/6.1.html#d2e1364](https://github.com/andygrunwald//docs.atlassian.com/DAC/rest/jira/6.1.html/issues/d2e1364)
* **IssueService:** allow empty JQL ([#268](https://github.com/andygrunwald/go-jira/issues/268)) ([4b91cf2](https://github.com/andygrunwald/go-jira/commit/4b91cf2b135355de7ecee41727c3e65f4e7067bc))
* **project:** Add cronjob to check for stale issues ([#287](https://github.com/andygrunwald/go-jira/issues/287)) ([2096b04](https://github.com/andygrunwald/go-jira/commit/2096b04e52b434c1fb1c841bab487a94674a271e))
* **project:** Add GitHub Actions testing workflow ([#289](https://github.com/andygrunwald/go-jira/issues/289)) ([80c0282](https://github.com/andygrunwald/go-jira/commit/80c02828ca9e4eb0e4a1877275baae14d330a2d9)), closes [#290](https://github.com/andygrunwald/go-jira/issues/290)
* **project:** Add workflow to greet new contributors ([#288](https://github.com/andygrunwald/go-jira/issues/288)) ([c357b61](https://github.com/andygrunwald/go-jira/commit/c357b61a40f62a919ebd94a555390958f99c8db7))
### Bug Fixes
* change millisecond time format ([8c77107](https://github.com/andygrunwald/go-jira/commit/8c77107df3757c4ec5eae6e9d7c018618e708bfa))
* paging with load balancer going to endless loop ([19d3fc0](https://github.com/andygrunwald/go-jira/commit/19d3fc0aecde547ffe1ab547c5ffb6c7972d387c)), closes [#260](https://github.com/andygrunwald/go-jira/issues/260)
* **issue:** IssueService.Search() with a not empty JQL triggers 400 bad request ([#292](https://github.com/andygrunwald/go-jira/issues/292)) ([8b64c7f](https://github.com/andygrunwald/go-jira/commit/8b64c7f005fbceb11fa43a7aff3de61eb3166fca)), closes [#291](https://github.com/andygrunwald/go-jira/issues/291)
* **IssueService.GetWatchers:** UserService.GetByAccountID support accountId params ([436469b](https://github.com/andygrunwald/go-jira/commit/436469b62d4d62037f380b38c918a13f4a5f0ab2))
* **product:** Make product naming consistent, rename JIRA to Jira ([#286](https://github.com/andygrunwald/go-jira/issues/286)) ([146229d](https://github.com/andygrunwald/go-jira/commit/146229d2ab58a3fb128ddc8dcbe03aff72e20857)), closes [#284](https://github.com/andygrunwald/go-jira/issues/284)
* **tests:** Fix TestIssueService_PostAttachment unit test ([f6b1dca](https://github.com/andygrunwald/go-jira/commit/f6b1dcafcfdd8fe69f842b1053c4030da6c97c7f))
* removing the use of username field in searching for users ([#297](https://github.com/andygrunwald/go-jira/issues/297)) ([f50cb07](https://github.com/andygrunwald/go-jira/commit/f50cb07b297d79138b13e5ab49ea33965d32f5c1))
## [1.12.0](https://github.com/andygrunwald/go-jira/compare/v1.11.1...v1.12.0) (2019-12-14)
@ -76,6 +102,3 @@ All notable changes to this project will be documented in this file. See [standa
* Add ResolutionService to retrieve resolutions ([fb1ce22](https://github.com/andygrunwald/go-jira/commit/fb1ce22))
* Add status category constants ([6223ddd](https://github.com/andygrunwald/go-jira/commit/6223ddd))
* Add StatusCategory GetList ([049a756](https://github.com/andygrunwald/go-jira/commit/049a756))

@ -1,2 +1,25 @@
test:
go test -v ./...
.DEFAULT_GOAL := help
.PHONY: help
help: ## Outputs the help.
@grep -E '^[a-zA-Z_-]+:.*?## .*$$' $(MAKEFILE_LIST) | sort | awk 'BEGIN {FS = ":.*?## "}; {printf "\033[36m%-30s\033[0m %s\n", $$1, $$2}'
.PHONY: test
test: ## Runs all unit, integration and example tests.
go test -race -v ./...
.PHONY: vet
vet: ## Runs go vet (to detect suspicious constructs).
go vet ./...
.PHONY: fmt
fmt: ## Runs go fmt (to check for go coding guidelines).
gofmt -d -s .
.PHONY: staticcheck
staticcheck: ## Runs static analysis to prevend bugs, foster code simplicity, performance and editor integration.
go get -u honnef.co/go/tools/cmd/staticcheck
staticcheck ./...
.PHONY: all
all: test vet fmt staticcheck ## Runs all source code quality targets (like test, vet, fmt, staticcheck)

@ -1,15 +0,0 @@
# PR Description
_What does this fix or add?_
# Checklist
* [ ] Tests added
* [ ] Good Path
* [ ] Error Path
* [ ] Commits follow conventions described here:
* [ ] [https://conventionalcommits.org/en/v1.0.0-beta.4/#summary](https://conventionalcommits.org/en/v1.0.0-beta.4/#summary)
* [ ] [https://chris.beams.io/posts/git-commit/#seven-rules](https://chris.beams.io/posts/git-commit/#seven-rules)
* [ ] Commits are squashed such that
* [ ] There is 1 commit per isolated change
* [ ] I've not made extraneous commits/changes that are unrelated to my change.

@ -1,26 +1,29 @@
# go-jira
[![GoDoc](https://godoc.org/github.com/andygrunwald/go-jira?status.svg)](https://godoc.org/github.com/andygrunwald/go-jira)
[![Build Status](https://travis-ci.org/andygrunwald/go-jira.svg?branch=master)](https://travis-ci.org/andygrunwald/go-jira)
[![Build Status](https://github.com/andygrunwald/go-jira/actions/workflows/testing.yml/badge.svg)](https://github.com/andygrunwald/go-jira/actions/workflows/testing.yml)
[![Go Report Card](https://goreportcard.com/badge/github.com/andygrunwald/go-jira)](https://goreportcard.com/report/github.com/andygrunwald/go-jira)
[Go](https://golang.org/) client library for [Atlassian JIRA](https://www.atlassian.com/software/jira).
[Go](https://golang.org/) client library for [Atlassian Jira](https://www.atlassian.com/software/jira).
![Go client library for Atlassian JIRA](./img/logo_small.png "Go client library for Atlassian JIRA.")
![Go client library for Atlassian Jira](./img/logo_small.png "Go client library for Atlassian Jira.")
## Features
* Authentication (HTTP Basic, OAuth, Session Cookie)
* Create and retrieve issues
* Create and retrieve issue transitions (status updates)
* Call every API endpoint of the JIRA, even if it is not directly implemented in this library
* Call every API endpoint of the Jira, even if it is not directly implemented in this library
This package is not JIRA API complete (yet), but you can call every API endpoint you want. See [Call a not implemented API endpoint](#call-a-not-implemented-api-endpoint) how to do this. For all possible API endpoints of JIRA have a look at [latest JIRA REST API documentation](https://docs.atlassian.com/jira/REST/latest/).
This package is not Jira API complete (yet), but you can call every API endpoint you want. See [Call a not implemented API endpoint](#call-a-not-implemented-api-endpoint) how to do this. For all possible API endpoints of Jira have a look at [latest Jira REST API documentation](https://docs.atlassian.com/jira/REST/latest/).
## Requirements
* Go >= 1.8
* JIRA v6.3.4 & v7.1.2.
* Go >= 1.14
* Jira v6.3.4 & v7.1.2.
Note that we also run our tests against 1.13, though only the last two versions
of Go are officially supported.
## Installation
@ -52,7 +55,7 @@ go test -v ./...
Please have a look at the [GoDoc documentation](https://godoc.org/github.com/andygrunwald/go-jira) for a detailed API description.
The [latest JIRA REST API documentation](https://docs.atlassian.com/jira/REST/latest/) was the base document for this package.
The [latest Jira REST API documentation](https://docs.atlassian.com/jira/REST/latest/) was the base document for this package.
## Examples
@ -92,9 +95,11 @@ an `http.Client`. That client can then be passed into the `NewClient` function
For convenience, capability for basic and cookie-based authentication is included in the main library.
#### Basic auth example
#### Token (Jira on Atlassian Cloud)
A more thorough, [runnable example](examples/basicauth/main.go) is provided in the examples directory. **It's worth noting that using passwords in basic auth is now deprecated and will be removed.** Jira gives you the ability to [create tokens now.](https://confluence.atlassian.com/cloud/api-tokens-938839638.html)
Token-based authentication uses the basic authentication scheme, with a user-generated API token in place of a user's password. You can generate a token for your user [here](https://id.atlassian.com/manage-profile/security/api-tokens). Additional information about Atlassian Cloud API tokens can be found [here](https://confluence.atlassian.com/cloud/api-tokens-938839638.html).
A more thorough, [runnable example](examples/basicauth/main.go) is provided in the examples directory.
```go
func main() {
@ -111,14 +116,15 @@ func main() {
}
```
#### Authenticate with session cookie [DEPRECATED]
#### Basic (self-hosted Jira)
JIRA [deprecated this authentication method.](https://developer.atlassian.com/cloud/jira/platform/deprecation-notice-basic-auth-and-cookie-based-auth/) It's not longer available for use.
Password-based API authentication works for self-hosted Jira **only**, and has been [deprecated for users of Atlassian Cloud](https://developer.atlassian.com/cloud/jira/platform/deprecation-notice-basic-auth-and-cookie-based-auth/).
The above token authentication example may be used, substituting a user's password for a generated token.
#### Authenticate with OAuth
If you want to connect via OAuth to your JIRA Cloud instance checkout the [example of using OAuth authentication with JIRA in Go](https://gist.github.com/Lupus/edafe9a7c5c6b13407293d795442fe67) by [@Lupus](https://github.com/Lupus).
If you want to connect via OAuth to your Jira Cloud instance checkout the [example of using OAuth authentication with Jira in Go](https://gist.github.com/Lupus/edafe9a7c5c6b13407293d795442fe67) by [@Lupus](https://github.com/Lupus).
For more details have a look at the [issue #56](https://github.com/andygrunwald/go-jira/issues/56).
@ -173,11 +179,62 @@ func main() {
}
```
### Change an issue status
This is how one can change an issue status. In this example, we change the issue from "To Do" to "In Progress."
```go
package main
import (
"fmt"
"github.com/andygrunwald/go-jira"
)
func main() {
base := "https://my.jira.com"
tp := jira.BasicAuthTransport{
Username: "username",
Password: "token",
}
jiraClient, err := jira.NewClient(tp.Client(), base)
if err != nil {
panic(err)
}
issue, _, _ := jiraClient.Issue.Get("FART-1", nil)
currentStatus := issue.Fields.Status.Name
fmt.Printf("Current status: %s\n", currentStatus)
var transitionID string
possibleTransitions, _, _ := jiraClient.Issue.GetTransitions("FART-1")
for _, v := range possibleTransitions {
if v.Name == "In Progress" {
transitionID = v.ID
break
}
}
jiraClient.Issue.DoTransition("FART-1", transitionID)
issue, _, _ = jiraClient.Issue.Get(testIssueID, nil)
fmt.Printf("Status after transition: %+v\n", issue.Fields.Status.Name)
}
```
### Get all the issues for JQL with Pagination
Jira API has limit on maxResults it can return. You may have a usecase where you need to get all issues for given JQL.
This example shows reference implementation of GetAllIssues function which does pagination on Jira API to get all the issues for given JQL
please look at [Pagination Example](https://github.com/andygrunwald/go-jira/blob/master/examples/pagination/main.go)
### Call a not implemented API endpoint
Not all API endpoints of the JIRA API are implemented into *go-jira*.
Not all API endpoints of the Jira API are implemented into *go-jira*.
But you can call them anyway:
Lets get all public projects of [Atlassian`s JIRA instance](https://jira.atlassian.com/).
Lets get all public projects of [Atlassian`s Jira instance](https://jira.atlassian.com/).
```go
package main
@ -209,7 +266,7 @@ func main() {
// ...
// BAM: Bamboo
// BAMJ: Bamboo JIRA Plugin
// BAMJ: Bamboo Jira Plugin
// CLOV: Clover
// CONF: Confluence
// ...
@ -218,7 +275,7 @@ func main() {
## Implementations
* [andygrunwald/jitic](https://github.com/andygrunwald/jitic) - The JIRA Ticket Checker
* [andygrunwald/jitic](https://github.com/andygrunwald/jitic) - The Jira Ticket Checker
## Code structure
@ -226,7 +283,7 @@ The code structure of this package was inspired by [google/go-github](https://gi
There is one main part (the client).
Based on this main client the other endpoints, like Issues or Authentication are extracted in services. E.g. `IssueService` or `AuthenticationService`.
These services own a responsibility of the single endpoints / usecases of JIRA.
These services own a responsibility of the single endpoints / usecases of Jira.
## Contribution
@ -258,7 +315,7 @@ You can read more about them at https://developer.atlassian.com/blog/2016/04/clo
## Releasing
Install `standard-version`
Install [standard-version](https://github.com/conventional-changelog/standard-version)
```bash
npm i -g standard-version
```

@ -1,6 +1,7 @@
package jira
import (
"context"
"encoding/json"
"fmt"
"io/ioutil"
@ -14,9 +15,9 @@ const (
authTypeSession = 2
)
// AuthenticationService handles authentication for the JIRA instance / API.
// AuthenticationService handles authentication for the Jira instance / API.
//
// JIRA API docs: https://docs.atlassian.com/jira/REST/latest/#authentication
// Jira API docs: https://docs.atlassian.com/jira/REST/latest/#authentication
type AuthenticationService struct {
client *Client
@ -30,7 +31,7 @@ type AuthenticationService struct {
password string
}
// Session represents a Session JSON response by the JIRA API.
// Session represents a Session JSON response by the Jira API.
type Session struct {
Self string `json:"self,omitempty"`
Name string `json:"name,omitempty"`
@ -47,16 +48,16 @@ type Session struct {
Cookies []*http.Cookie
}
// AcquireSessionCookie creates a new session for a user in JIRA.
// Once a session has been successfully created it can be used to access any of JIRA's remote APIs and also the web UI by passing the appropriate HTTP Cookie header.
// AcquireSessionCookieWithContext creates a new session for a user in Jira.
// Once a session has been successfully created it can be used to access any of Jira's remote APIs and also the web UI by passing the appropriate HTTP Cookie header.
// The header will by automatically applied to every API request.
// Note that it is generally preferrable to use HTTP BASIC authentication with the REST API.
// However, this resource may be used to mimic the behaviour of JIRA's log-in page (e.g. to display log-in errors to a user).
// However, this resource may be used to mimic the behaviour of Jira's log-in page (e.g. to display log-in errors to a user).
//
// JIRA API docs: https://docs.atlassian.com/jira/REST/latest/#auth/1/session
// Jira API docs: https://docs.atlassian.com/jira/REST/latest/#auth/1/session
//
// Deprecated: Use CookieAuthTransport instead
func (s *AuthenticationService) AcquireSessionCookie(username, password string) (bool, error) {
func (s *AuthenticationService) AcquireSessionCookieWithContext(ctx context.Context, username, password string) (bool, error) {
apiEndpoint := "rest/auth/1/session"
body := struct {
Username string `json:"username"`
@ -66,7 +67,7 @@ func (s *AuthenticationService) AcquireSessionCookie(username, password string)
password,
}
req, err := s.client.NewRequest("POST", apiEndpoint, body)
req, err := s.client.NewRequestWithContext(ctx, "POST", apiEndpoint, body)
if err != nil {
return false, err
}
@ -79,10 +80,10 @@ func (s *AuthenticationService) AcquireSessionCookie(username, password string)
}
if err != nil {
return false, fmt.Errorf("Auth at JIRA instance failed (HTTP(S) request). %s", err)
return false, fmt.Errorf("auth at Jira instance failed (HTTP(S) request). %s", err)
}
if resp != nil && resp.StatusCode != 200 {
return false, fmt.Errorf("Auth at JIRA instance failed (HTTP(S) request). Status code: %d", resp.StatusCode)
return false, fmt.Errorf("auth at Jira instance failed (HTTP(S) request). Status code: %d", resp.StatusCode)
}
s.client.session = session
@ -91,7 +92,14 @@ func (s *AuthenticationService) AcquireSessionCookie(username, password string)
return true, nil
}
// SetBasicAuth sets username and password for the basic auth against the JIRA instance.
// AcquireSessionCookie wraps AcquireSessionCookieWithContext using the background context.
//
// Deprecated: Use CookieAuthTransport instead
func (s *AuthenticationService) AcquireSessionCookie(username, password string) (bool, error) {
return s.AcquireSessionCookieWithContext(context.Background(), username, password)
}
// SetBasicAuth sets username and password for the basic auth against the Jira instance.
//
// Deprecated: Use BasicAuthTransport instead
func (s *AuthenticationService) SetBasicAuth(username, password string) {
@ -100,7 +108,7 @@ func (s *AuthenticationService) SetBasicAuth(username, password string) {
s.authType = authTypeBasic
}
// Authenticated reports if the current Client has authentication details for JIRA
// Authenticated reports if the current Client has authentication details for Jira
func (s *AuthenticationService) Authenticated() bool {
if s != nil {
if s.authType == authTypeSession {
@ -113,29 +121,29 @@ func (s *AuthenticationService) Authenticated() bool {
return false
}
// Logout logs out the current user that has been authenticated and the session in the client is destroyed.
// LogoutWithContext logs out the current user that has been authenticated and the session in the client is destroyed.
//
// JIRA API docs: https://docs.atlassian.com/jira/REST/latest/#auth/1/session
// Jira API docs: https://docs.atlassian.com/jira/REST/latest/#auth/1/session
//
// Deprecated: Use CookieAuthTransport to create base client. Logging out is as simple as not using the
// client anymore
func (s *AuthenticationService) Logout() error {
func (s *AuthenticationService) LogoutWithContext(ctx context.Context) error {
if s.authType != authTypeSession || s.client.session == nil {
return fmt.Errorf("no user is authenticated")
}
apiEndpoint := "rest/auth/1/session"
req, err := s.client.NewRequest("DELETE", apiEndpoint, nil)
req, err := s.client.NewRequestWithContext(ctx, "DELETE", apiEndpoint, nil)
if err != nil {
return fmt.Errorf("Creating the request to log the user out failed : %s", err)
return fmt.Errorf("creating the request to log the user out failed : %s", err)
}
resp, err := s.client.Do(req, nil)
if err != nil {
return fmt.Errorf("Error sending the logout request: %s", err)
return fmt.Errorf("error sending the logout request: %s", err)
}
if resp.StatusCode != 204 {
return fmt.Errorf("The logout was unsuccessful with status %d", resp.StatusCode)
return fmt.Errorf("the logout was unsuccessful with status %d", resp.StatusCode)
}
// If logout successful, delete session
@ -145,43 +153,56 @@ func (s *AuthenticationService) Logout() error {
}
// GetCurrentUser gets the details of the current user.
// Logout wraps LogoutWithContext using the background context.
//
// JIRA API docs: https://docs.atlassian.com/jira/REST/latest/#auth/1/session
func (s *AuthenticationService) GetCurrentUser() (*Session, error) {
// Deprecated: Use CookieAuthTransport to create base client. Logging out is as simple as not using the
// client anymore
func (s *AuthenticationService) Logout() error {
return s.LogoutWithContext(context.Background())
}
// GetCurrentUserWithContext gets the details of the current user.
//
// Jira API docs: https://docs.atlassian.com/jira/REST/latest/#auth/1/session
func (s *AuthenticationService) GetCurrentUserWithContext(ctx context.Context) (*Session, error) {
if s == nil {
return nil, fmt.Errorf("AUthenticaiton Service is not instantiated")
return nil, fmt.Errorf("authentication Service is not instantiated")
}
if s.authType != authTypeSession || s.client.session == nil {
return nil, fmt.Errorf("No user is authenticated yet")
return nil, fmt.Errorf("no user is authenticated yet")
}
apiEndpoint := "rest/auth/1/session"
req, err := s.client.NewRequest("GET", apiEndpoint, nil)
req, err := s.client.NewRequestWithContext(ctx, "GET", apiEndpoint, nil)
if err != nil {
return nil, fmt.Errorf("Could not create request for getting user info : %s", err)
return nil, fmt.Errorf("could not create request for getting user info : %s", err)
}
resp, err := s.client.Do(req, nil)
if err != nil {
return nil, fmt.Errorf("Error sending request to get user info : %s", err)
return nil, fmt.Errorf("error sending request to get user info : %s", err)
}
if resp.StatusCode != 200 {
return nil, fmt.Errorf("Getting user info failed with status : %d", resp.StatusCode)
return nil, fmt.Errorf("getting user info failed with status : %d", resp.StatusCode)
}
defer resp.Body.Close()
ret := new(Session)
data, err := ioutil.ReadAll(resp.Body)
if err != nil {
return nil, fmt.Errorf("Couldn't read body from the response : %s", err)
return nil, fmt.Errorf("couldn't read body from the response : %s", err)
}
err = json.Unmarshal(data, &ret)
if err != nil {
return nil, fmt.Errorf("Could not unmarshall received user info : %s", err)
return nil, fmt.Errorf("could not unmarshall received user info : %s", err)
}
return ret, nil
}
// GetCurrentUser wraps GetCurrentUserWithContext using the background context.
func (s *AuthenticationService) GetCurrentUser() (*Session, error) {
return s.GetCurrentUserWithContext(context.Background())
}

@ -1,14 +1,15 @@
package jira
import (
"context"
"fmt"
"strconv"
"time"
)
// BoardService handles Agile Boards for the JIRA instance / API.
// BoardService handles Agile Boards for the Jira instance / API.
//
// JIRA API docs: https://docs.atlassian.com/jira-software/REST/server/
// Jira API docs: https://docs.atlassian.com/jira-software/REST/server/
type BoardService struct {
client *Client
}
@ -22,7 +23,7 @@ type BoardsList struct {
Values []Board `json:"values" structs:"values"`
}
// Board represents a JIRA agile board
// Board represents a Jira agile board
type Board struct {
ID int `json:"id,omitempty" structs:"id,omitempty"`
Self string `json:"self,omitempty" structs:"self,omitempty"`
@ -62,7 +63,7 @@ type SprintsList struct {
Values []Sprint `json:"values" structs:"values"`
}
// Sprint represents a sprint on JIRA agile board
// Sprint represents a sprint on Jira agile board
type Sprint struct {
ID int `json:"id" structs:"id"`
Name string `json:"name" structs:"name"`
@ -124,16 +125,16 @@ type BoardConfigurationColumnStatus struct {
Self string `json:"self"`
}
// GetAllBoards will returns all boards. This only includes boards that the user has permission to view.
// GetAllBoardsWithContext will returns all boards. This only includes boards that the user has permission to view.
//
// JIRA API docs: https://docs.atlassian.com/jira-software/REST/cloud/#agile/1.0/board-getAllBoards
func (s *BoardService) GetAllBoards(opt *BoardListOptions) (*BoardsList, *Response, error) {
// Jira API docs: https://docs.atlassian.com/jira-software/REST/cloud/#agile/1.0/board-getAllBoards
func (s *BoardService) GetAllBoardsWithContext(ctx context.Context, opt *BoardListOptions) (*BoardsList, *Response, error) {
apiEndpoint := "rest/agile/1.0/board"
url, err := addOptions(apiEndpoint, opt)
if err != nil {
return nil, nil, err
}
req, err := s.client.NewRequest("GET", url, nil)
req, err := s.client.NewRequestWithContext(ctx, "GET", url, nil)
if err != nil {
return nil, nil, err
}
@ -148,13 +149,18 @@ func (s *BoardService) GetAllBoards(opt *BoardListOptions) (*BoardsList, *Respon
return boards, resp, err
}
// GetBoard will returns the board for the given boardID.
// GetAllBoards wraps GetAllBoardsWithContext using the background context.
func (s *BoardService) GetAllBoards(opt *BoardListOptions) (*BoardsList, *Response, error) {
return s.GetAllBoardsWithContext(context.Background(), opt)
}
// GetBoardWithContext will returns the board for the given boardID.
// This board will only be returned if the user has permission to view it.
//
// JIRA API docs: https://docs.atlassian.com/jira-software/REST/cloud/#agile/1.0/board-getBoard
func (s *BoardService) GetBoard(boardID int) (*Board, *Response, error) {
// Jira API docs: https://docs.atlassian.com/jira-software/REST/cloud/#agile/1.0/board-getBoard
func (s *BoardService) GetBoardWithContext(ctx context.Context, boardID int) (*Board, *Response, error) {
apiEndpoint := fmt.Sprintf("rest/agile/1.0/board/%v", boardID)
req, err := s.client.NewRequest("GET", apiEndpoint, nil)
req, err := s.client.NewRequestWithContext(ctx, "GET", apiEndpoint, nil)
if err != nil {
return nil, nil, err
}
@ -169,17 +175,22 @@ func (s *BoardService) GetBoard(boardID int) (*Board, *Response, error) {
return board, resp, nil
}
// CreateBoard creates a new board. Board name, type and filter Id is required.
// GetBoard wraps GetBoardWithContext using the background context.
func (s *BoardService) GetBoard(boardID int) (*Board, *Response, error) {
return s.GetBoardWithContext(context.Background(), boardID)
}
// CreateBoardWithContext creates a new board. Board name, type and filter Id is required.
// name - Must be less than 255 characters.
// type - Valid values: scrum, kanban
// filterId - Id of a filter that the user has permissions to view.
// Note, if the user does not have the 'Create shared objects' permission and tries to create a shared board, a private
// board will be created instead (remember that board sharing depends on the filter sharing).
//
// JIRA API docs: https://docs.atlassian.com/jira-software/REST/cloud/#agile/1.0/board-createBoard
func (s *BoardService) CreateBoard(board *Board) (*Board, *Response, error) {
// Jira API docs: https://docs.atlassian.com/jira-software/REST/cloud/#agile/1.0/board-createBoard
func (s *BoardService) CreateBoardWithContext(ctx context.Context, board *Board) (*Board, *Response, error) {
apiEndpoint := "rest/agile/1.0/board"
req, err := s.client.NewRequest("POST", apiEndpoint, board)
req, err := s.client.NewRequestWithContext(ctx, "POST", apiEndpoint, board)
if err != nil {
return nil, nil, err
}
@ -194,12 +205,17 @@ func (s *BoardService) CreateBoard(board *Board) (*Board, *Response, error) {
return responseBoard, resp, nil
}
// DeleteBoard will delete an agile board.
// CreateBoard wraps CreateBoardWithContext using the background context.
func (s *BoardService) CreateBoard(board *Board) (*Board, *Response, error) {
return s.CreateBoardWithContext(context.Background(), board)
}
// DeleteBoardWithContext will delete an agile board.
//
// JIRA API docs: https://docs.atlassian.com/jira-software/REST/cloud/#agile/1.0/board-deleteBoard
func (s *BoardService) DeleteBoard(boardID int) (*Board, *Response, error) {
// Jira API docs: https://docs.atlassian.com/jira-software/REST/cloud/#agile/1.0/board-deleteBoard
func (s *BoardService) DeleteBoardWithContext(ctx context.Context, boardID int) (*Board, *Response, error) {
apiEndpoint := fmt.Sprintf("rest/agile/1.0/board/%v", boardID)
req, err := s.client.NewRequest("DELETE", apiEndpoint, nil)
req, err := s.client.NewRequestWithContext(ctx, "DELETE", apiEndpoint, nil)
if err != nil {
return nil, nil, err
}
@ -211,11 +227,16 @@ func (s *BoardService) DeleteBoard(boardID int) (*Board, *Response, error) {
return nil, resp, err
}
// GetAllSprints will return all sprints from a board, for a given board Id.
// DeleteBoard wraps DeleteBoardWithContext using the background context.
func (s *BoardService) DeleteBoard(boardID int) (*Board, *Response, error) {
return s.DeleteBoardWithContext(context.Background(), boardID)
}
// GetAllSprintsWithContext will return all sprints from a board, for a given board Id.
// This only includes sprints that the user has permission to view.
//
// JIRA API docs: https://docs.atlassian.com/jira-software/REST/cloud/#agile/1.0/board/{boardId}/sprint
func (s *BoardService) GetAllSprints(boardID string) ([]Sprint, *Response, error) {
// Jira API docs: https://docs.atlassian.com/jira-software/REST/cloud/#agile/1.0/board/{boardId}/sprint
func (s *BoardService) GetAllSprintsWithContext(ctx context.Context, boardID string) ([]Sprint, *Response, error) {
id, err := strconv.Atoi(boardID)
if err != nil {
return nil, nil, err
@ -229,17 +250,22 @@ func (s *BoardService) GetAllSprints(boardID string) ([]Sprint, *Response, error
return result.Values, response, nil
}
// GetAllSprintsWithOptions will return sprints from a board, for a given board Id and filtering options
// GetAllSprints wraps GetAllSprintsWithContext using the background context.
func (s *BoardService) GetAllSprints(boardID string) ([]Sprint, *Response, error) {
return s.GetAllSprintsWithContext(context.Background(), boardID)
}
// GetAllSprintsWithOptionsWithContext will return sprints from a board, for a given board Id and filtering options
// This only includes sprints that the user has permission to view.
//
// JIRA API docs: https://docs.atlassian.com/jira-software/REST/cloud/#agile/1.0/board/{boardId}/sprint
func (s *BoardService) GetAllSprintsWithOptions(boardID int, options *GetAllSprintsOptions) (*SprintsList, *Response, error) {
// Jira API docs: https://docs.atlassian.com/jira-software/REST/cloud/#agile/1.0/board/{boardId}/sprint
func (s *BoardService) GetAllSprintsWithOptionsWithContext(ctx context.Context, boardID int, options *GetAllSprintsOptions) (*SprintsList, *Response, error) {
apiEndpoint := fmt.Sprintf("rest/agile/1.0/board/%d/sprint", boardID)
url, err := addOptions(apiEndpoint, options)
if err != nil {
return nil, nil, err
}
req, err := s.client.NewRequest("GET", url, nil)
req, err := s.client.NewRequestWithContext(ctx, "GET", url, nil)
if err != nil {
return nil, nil, err
}
@ -253,12 +279,17 @@ func (s *BoardService) GetAllSprintsWithOptions(boardID int, options *GetAllSpri
return result, resp, err
}
// GetBoardConfiguration will return a board configuration for a given board Id
// GetAllSprintsWithOptions wraps GetAllSprintsWithOptionsWithContext using the background context.
func (s *BoardService) GetAllSprintsWithOptions(boardID int, options *GetAllSprintsOptions) (*SprintsList, *Response, error) {
return s.GetAllSprintsWithOptionsWithContext(context.Background(), boardID, options)
}
// GetBoardConfigurationWithContext will return a board configuration for a given board Id
// Jira API docs:https://developer.atlassian.com/cloud/jira/software/rest/#api-rest-agile-1-0-board-boardId-configuration-get
func (s *BoardService) GetBoardConfiguration(boardID int) (*BoardConfiguration, *Response, error) {
func (s *BoardService) GetBoardConfigurationWithContext(ctx context.Context, boardID int) (*BoardConfiguration, *Response, error) {
apiEndpoint := fmt.Sprintf("rest/agile/1.0/board/%d/configuration", boardID)
req, err := s.client.NewRequest("GET", apiEndpoint, nil)
req, err := s.client.NewRequestWithContext(ctx, "GET", apiEndpoint, nil)
if err != nil {
return nil, nil, err
@ -273,3 +304,8 @@ func (s *BoardService) GetBoardConfiguration(boardID int) (*BoardConfiguration,
return result, resp, err
}
// GetBoardConfiguration wraps GetBoardConfigurationWithContext using the background context.
func (s *BoardService) GetBoardConfiguration(boardID int) (*BoardConfiguration, *Response, error) {
return s.GetBoardConfigurationWithContext(context.Background(), boardID)
}

@ -1,13 +1,14 @@
package jira
// ComponentService handles components for the JIRA instance / API.
//
// JIRA API docs: https://docs.atlassian.com/software/jira/docs/api/REST/7.10.1/#api/2/component
import "context"
// ComponentService handles components for the Jira instance / API.//
// Jira API docs: https://docs.atlassian.com/software/jira/docs/api/REST/7.10.1/#api/2/component
type ComponentService struct {
client *Client
}
// CreateComponentOptions are passed to the ComponentService.Create function to create a new JIRA component
// CreateComponentOptions are passed to the ComponentService.Create function to create a new Jira component
type CreateComponentOptions struct {
Name string `json:"name,omitempty" structs:"name,omitempty"`
Description string `json:"description,omitempty" structs:"description,omitempty"`
@ -19,10 +20,10 @@ type CreateComponentOptions struct {
ProjectID int `json:"projectId,omitempty" structs:"projectId,omitempty"`
}
// Create creates a new JIRA component based on the given options.
func (s *ComponentService) Create(options *CreateComponentOptions) (*ProjectComponent, *Response, error) {
// CreateWithContext creates a new Jira component based on the given options.
func (s *ComponentService) CreateWithContext(ctx context.Context, options *CreateComponentOptions) (*ProjectComponent, *Response, error) {
apiEndpoint := "rest/api/2/component"
req, err := s.client.NewRequest("POST", apiEndpoint, options)
req, err := s.client.NewRequestWithContext(ctx, "POST", apiEndpoint, options)
if err != nil {
return nil, nil, err
}
@ -36,3 +37,8 @@ func (s *ComponentService) Create(options *CreateComponentOptions) (*ProjectComp
return component, resp, nil
}
// Create wraps CreateWithContext using the background context.
func (s *ComponentService) Create(options *CreateComponentOptions) (*ProjectComponent, *Response, error) {
return s.CreateWithContext(context.Background(), options)
}

@ -10,7 +10,7 @@ import (
"github.com/pkg/errors"
)
// Error message from JIRA
// Error message from Jira
// See https://docs.atlassian.com/jira/REST/cloud/#error-responses
type Error struct {
HTTPError error
@ -34,13 +34,13 @@ func NewJiraError(resp *Response, httpError error) error {
if strings.HasPrefix(contentType, "application/json") {
err = json.Unmarshal(body, &jerr)
if err != nil {
httpError = errors.Wrap(errors.New("Could not parse JSON"), httpError.Error())
httpError = errors.Wrap(errors.New("could not parse JSON"), httpError.Error())
return errors.Wrap(err, httpError.Error())
}
} else {
if httpError == nil {
return fmt.Errorf("Got Response Status %s:%s", resp.Status, string(body))
}
if httpError == nil {
return fmt.Errorf("got response status %s:%s", resp.Status, string(body))
}
return errors.Wrap(httpError, fmt.Sprintf("%s: %s", resp.Status, string(body)))
}

@ -1,13 +1,15 @@
package jira
// FieldService handles fields for the JIRA instance / API.
import "context"
// FieldService handles fields for the Jira instance / API.
//
// JIRA API docs: https://developer.atlassian.com/cloud/jira/platform/rest/#api-Field
// Jira API docs: https://developer.atlassian.com/cloud/jira/platform/rest/#api-Field
type FieldService struct {
client *Client
}
// Field represents a field of a JIRA issue.
// Field represents a field of a Jira issue.
type Field struct {
ID string `json:"id,omitempty" structs:"id,omitempty"`
Key string `json:"key,omitempty" structs:"key,omitempty"`
@ -19,17 +21,22 @@ type Field struct {
Schema FieldSchema `json:"schema,omitempty" structs:"schema,omitempty"`
}
// FieldSchema represents a schema of a Jira field.
// Documentation: https://developer.atlassian.com/cloud/jira/platform/rest/v2/api-group-issue-fields/#api-rest-api-2-field-get
type FieldSchema struct {
Type string `json:"type,omitempty" structs:"type,omitempty"`
System string `json:"system,omitempty" structs:"system,omitempty"`
Type string `json:"type,omitempty" structs:"type,omitempty"`
Items string `json:"items,omitempty" structs:"items,omitempty"`
Custom string `json:"custom,omitempty" structs:"custom,omitempty"`
System string `json:"system,omitempty" structs:"system,omitempty"`
CustomID int64 `json:"customId,omitempty" structs:"customId,omitempty"`
}
// GetList gets all fields from JIRA
// GetListWithContext gets all fields from Jira
//
// JIRA API docs: https://developer.atlassian.com/cloud/jira/platform/rest/#api-api-2-field-get
func (s *FieldService) GetList() ([]Field, *Response, error) {
// Jira API docs: https://developer.atlassian.com/cloud/jira/platform/rest/#api-api-2-field-get
func (s *FieldService) GetListWithContext(ctx context.Context) ([]Field, *Response, error) {
apiEndpoint := "rest/api/2/field"
req, err := s.client.NewRequest("GET", apiEndpoint, nil)
req, err := s.client.NewRequestWithContext(ctx, "GET", apiEndpoint, nil)
if err != nil {
return nil, nil, err
}
@ -41,3 +48,8 @@ func (s *FieldService) GetList() ([]Field, *Response, error) {
}
return fieldList, resp, nil
}
// GetList wraps GetListWithContext using the background context.
func (s *FieldService) GetList() ([]Field, *Response, error) {
return s.GetListWithContext(context.Background())
}

@ -1,11 +1,15 @@
package jira
import "github.com/google/go-querystring/query"
import "fmt"
import (
"context"
"fmt"
// FilterService handles fields for the JIRA instance / API.
"github.com/google/go-querystring/query"
)
// FilterService handles fields for the Jira instance / API.
//
// JIRA API docs: https://developer.atlassian.com/cloud/jira/platform/rest/v3/#api-group-Filter
// Jira API docs: https://developer.atlassian.com/cloud/jira/platform/rest/v3/#api-group-Filter
type FilterService struct {
client *Client
}
@ -116,23 +120,21 @@ type FilterSearchOptions struct {
Expand string `url:"expand,omitempty"`
}
// GetList retrieves all filters from Jira
func (fs *FilterService) GetList() ([]*Filter, *Response, error) {
// GetListWithContext retrieves all filters from Jira
func (fs *FilterService) GetListWithContext(ctx context.Context) ([]*Filter, *Response, error) {
options := &GetQueryOptions{}
apiEndpoint := "rest/api/2/filter"
req, err := fs.client.NewRequest("GET", apiEndpoint, nil)
req, err := fs.client.NewRequestWithContext(ctx, "GET", apiEndpoint, nil)
if err != nil {
return nil, nil, err
}
if options != nil {
q, err := query.Values(options)
if err != nil {
return nil, nil, err
}
req.URL.RawQuery = q.Encode()
q, err := query.Values(options)
if err != nil {
return nil, nil, err
}
req.URL.RawQuery = q.Encode()
filters := []*Filter{}
resp, err := fs.client.Do(req, &filters)
@ -143,10 +145,15 @@ func (fs *FilterService) GetList() ([]*Filter, *Response, error) {
return filters, resp, err
}
// GetFavouriteList retrieves the user's favourited filters from Jira
func (fs *FilterService) GetFavouriteList() ([]*Filter, *Response, error) {
// GetList wraps GetListWithContext using the background context.
func (fs *FilterService) GetList() ([]*Filter, *Response, error) {
return fs.GetListWithContext(context.Background())
}
// GetFavouriteListWithContext retrieves the user's favourited filters from Jira
func (fs *FilterService) GetFavouriteListWithContext(ctx context.Context) ([]*Filter, *Response, error) {
apiEndpoint := "rest/api/2/filter/favourite"
req, err := fs.client.NewRequest("GET", apiEndpoint, nil)
req, err := fs.client.NewRequestWithContext(ctx, "GET", apiEndpoint, nil)
if err != nil {
return nil, nil, err
}
@ -159,10 +166,15 @@ func (fs *FilterService) GetFavouriteList() ([]*Filter, *Response, error) {
return filters, resp, err
}
// Get retrieves a single Filter from Jira
func (fs *FilterService) Get(filterID int) (*Filter, *Response, error) {
// GetFavouriteList wraps GetFavouriteListWithContext using the background context.
func (fs *FilterService) GetFavouriteList() ([]*Filter, *Response, error) {
return fs.GetFavouriteListWithContext(context.Background())
}
// GetWithContext retrieves a single Filter from Jira
func (fs *FilterService) GetWithContext(ctx context.Context, filterID int) (*Filter, *Response, error) {
apiEndpoint := fmt.Sprintf("rest/api/2/filter/%d", filterID)
req, err := fs.client.NewRequest("GET", apiEndpoint, nil)
req, err := fs.client.NewRequestWithContext(ctx, "GET", apiEndpoint, nil)
if err != nil {
return nil, nil, err
}
@ -176,16 +188,21 @@ func (fs *FilterService) Get(filterID int) (*Filter, *Response, error) {
return filter, resp, err
}
// GetMyFilters retrieves the my Filters.
// Get wraps GetWithContext using the background context.
func (fs *FilterService) Get(filterID int) (*Filter, *Response, error) {
return fs.GetWithContext(context.Background(), filterID)
}
// GetMyFiltersWithContext retrieves the my Filters.
//
// https://developer.atlassian.com/cloud/jira/platform/rest/v3/#api-rest-api-3-filter-my-get
func (fs *FilterService) GetMyFilters(opts *GetMyFiltersQueryOptions) ([]*Filter, *Response, error) {
func (fs *FilterService) GetMyFiltersWithContext(ctx context.Context, opts *GetMyFiltersQueryOptions) ([]*Filter, *Response, error) {
apiEndpoint := "rest/api/3/filter/my"
url, err := addOptions(apiEndpoint, opts)
if err != nil {
return nil, nil, err
}
req, err := fs.client.NewRequest("GET", url, nil)
req, err := fs.client.NewRequestWithContext(ctx, "GET", url, nil)
if err != nil {
return nil, nil, err
}
@ -199,16 +216,21 @@ func (fs *FilterService) GetMyFilters(opts *GetMyFiltersQueryOptions) ([]*Filter
return filters, resp, nil
}
// Search will search for filter according to the search options
// GetMyFilters wraps GetMyFiltersWithContext using the background context.
func (fs *FilterService) GetMyFilters(opts *GetMyFiltersQueryOptions) ([]*Filter, *Response, error) {
return fs.GetMyFiltersWithContext(context.Background(), opts)
}
// SearchWithContext will search for filter according to the search options
//
// JIRA API docs: https://developer.atlassian.com/cloud/jira/platform/rest/v3/#api-rest-api-3-filter-search-get
func (fs *FilterService) Search(opt *FilterSearchOptions) (*FiltersList, *Response, error) {
// Jira API docs: https://developer.atlassian.com/cloud/jira/platform/rest/v3/#api-rest-api-3-filter-search-get
func (fs *FilterService) SearchWithContext(ctx context.Context, opt *FilterSearchOptions) (*FiltersList, *Response, error) {
apiEndpoint := "rest/api/3/filter/search"
url, err := addOptions(apiEndpoint, opt)
if err != nil {
return nil, nil, err
}
req, err := fs.client.NewRequest("GET", url, nil)
req, err := fs.client.NewRequestWithContext(ctx, "GET", url, nil)
if err != nil {
return nil, nil, err
}
@ -222,3 +244,8 @@ func (fs *FilterService) Search(opt *FilterSearchOptions) (*FiltersList, *Respon
return filters, resp, err
}
// Search wraps SearchWithContext using the background context.
func (fs *FilterService) Search(opt *FilterSearchOptions) (*FiltersList, *Response, error) {
return fs.SearchWithContext(context.Background(), opt)
}

@ -1,13 +0,0 @@
module github.com/andygrunwald/go-jira
go 1.12
require (
github.com/dgrijalva/jwt-go v3.2.0+incompatible
github.com/fatih/structs v1.0.0
github.com/google/go-cmp v0.3.0
github.com/google/go-querystring v0.0.0-20170111101155-53e6ce116135
github.com/pkg/errors v0.8.0
github.com/trivago/tgo v1.0.1
golang.org/x/crypto v0.0.0-20190426145343-a29dc8fdc734
)

@ -1,20 +0,0 @@
github.com/dgrijalva/jwt-go v3.2.0+incompatible h1:7qlOGliEKZXTDg6OTjfoBKDXWrumCAMpl/TFQ4/5kLM=
github.com/dgrijalva/jwt-go v3.2.0+incompatible/go.mod h1:E3ru+11k8xSBh+hMPgOLZmtrrCbhqsmaPHjLKYnJCaQ=
github.com/fatih/structs v1.0.0 h1:BrX964Rv5uQ3wwS+KRUAJCBBw5PQmgJfJ6v4yly5QwU=
github.com/fatih/structs v1.0.0/go.mod h1:9NiDSp5zOcgEDl+j00MP/WkGVPOlPRLejGD8Ga6PJ7M=
github.com/google/go-cmp v0.3.0 h1:crn/baboCvb5fXaQ0IJ1SGTsTVrWpDsCWC8EGETZijY=
github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU=
github.com/google/go-querystring v0.0.0-20170111101155-53e6ce116135 h1:zLTLjkaOFEFIOxY5BWLFLwh+cL8vOBW4XJ2aqLE/Tf0=
github.com/google/go-querystring v0.0.0-20170111101155-53e6ce116135/go.mod h1:odCYkC5MyYFN7vkCjXpyrEuKhc/BUO6wN/zVPAxq5ck=
github.com/pkg/errors v0.8.0 h1:WdK/asTD0HN+q6hsWO3/vpuAkAr+tw6aNJNDFFf0+qw=
github.com/pkg/errors v0.8.0/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
github.com/trivago/tgo v1.0.1 h1:bxatjJIXNIpV18bucU4Uk/LaoxvxuOlp/oowRHyncLQ=
github.com/trivago/tgo v1.0.1/go.mod h1:w4dpD+3tzNIIiIfkWWa85w5/B77tlvdZckQ+6PkFnhc=
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
golang.org/x/crypto v0.0.0-20190426145343-a29dc8fdc734 h1:p/H982KKEjUnLJkM3tt/LemDnOc1GiZL5FCVlORJ5zo=
golang.org/x/crypto v0.0.0-20190426145343-a29dc8fdc734/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20190412213103-97732733099d h1:+R4KGOnez64A81RvjARKc4UT5/tI9ujCIVX+P5KiHuI=
golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=

@ -1,13 +1,14 @@
package jira
import (
"context"
"fmt"
"net/url"
)
// GroupService handles Groups for the JIRA instance / API.
// GroupService handles Groups for the Jira instance / API.
//
// JIRA API docs: https://docs.atlassian.com/jira/REST/server/#api/2/group
// Jira API docs: https://docs.atlassian.com/jira/REST/server/#api/2/group
type GroupService struct {
client *Client
}
@ -21,7 +22,7 @@ type groupMembersResult struct {
Members []GroupMember `json:"values"`
}
// Group represents a JIRA group
// Group represents a Jira group
type Group struct {
ID string `json:"id"`
Title string `json:"title"`
@ -58,16 +59,16 @@ type GroupSearchOptions struct {
IncludeInactiveUsers bool
}
// Get returns a paginated list of users who are members of the specified group and its subgroups.
// GetWithContext returns a paginated list of users who are members of the specified group and its subgroups.
// Users in the page are ordered by user names.
// User of this resource is required to have sysadmin or admin permissions.
//
// JIRA API docs: https://docs.atlassian.com/jira/REST/server/#api/2/group-getUsersFromGroup
// Jira API docs: https://docs.atlassian.com/jira/REST/server/#api/2/group-getUsersFromGroup
//
// WARNING: This API only returns the first page of group members
func (s *GroupService) Get(name string) ([]GroupMember, *Response, error) {
func (s *GroupService) GetWithContext(ctx context.Context, name string) ([]GroupMember, *Response, error) {
apiEndpoint := fmt.Sprintf("/rest/api/2/group/member?groupname=%s", url.QueryEscape(name))
req, err := s.client.NewRequest("GET", apiEndpoint, nil)
req, err := s.client.NewRequestWithContext(ctx, "GET", apiEndpoint, nil)
if err != nil {
return nil, nil, err
}
@ -81,12 +82,17 @@ func (s *GroupService) Get(name string) ([]GroupMember, *Response, error) {
return group.Members, resp, nil
}
// GetWithOptions returns a paginated list of members of the specified group and its subgroups.
// Get wraps GetWithContext using the background context.
func (s *GroupService) Get(name string) ([]GroupMember, *Response, error) {
return s.GetWithContext(context.Background(), name)
}
// GetWithOptionsWithContext returns a paginated list of members of the specified group and its subgroups.
// Users in the page are ordered by user names.
// User of this resource is required to have sysadmin or admin permissions.
//
// JIRA API docs: https://docs.atlassian.com/jira/REST/server/#api/2/group-getUsersFromGroup
func (s *GroupService) GetWithOptions(name string, options *GroupSearchOptions) ([]GroupMember, *Response, error) {
// Jira API docs: https://docs.atlassian.com/jira/REST/server/#api/2/group-getUsersFromGroup
func (s *GroupService) GetWithOptionsWithContext(ctx context.Context, name string, options *GroupSearchOptions) ([]GroupMember, *Response, error) {
var apiEndpoint string
if options == nil {
apiEndpoint = fmt.Sprintf("/rest/api/2/group/member?groupname=%s", url.QueryEscape(name))
@ -99,7 +105,7 @@ func (s *GroupService) GetWithOptions(name string, options *GroupSearchOptions)
options.IncludeInactiveUsers,
)
}
req, err := s.client.NewRequest("GET", apiEndpoint, nil)
req, err := s.client.NewRequestWithContext(ctx, "GET", apiEndpoint, nil)
if err != nil {
return nil, nil, err
}
@ -112,16 +118,21 @@ func (s *GroupService) GetWithOptions(name string, options *GroupSearchOptions)
return group.Members, resp, nil
}
// Add adds user to group
// GetWithOptions wraps GetWithOptionsWithContext using the background context.
func (s *GroupService) GetWithOptions(name string, options *GroupSearchOptions) ([]GroupMember, *Response, error) {
return s.GetWithOptionsWithContext(context.Background(), name, options)
}
// AddWithContext adds user to group
//
// JIRA API docs: https://docs.atlassian.com/jira/REST/cloud/#api/2/group-addUserToGroup
func (s *GroupService) Add(groupname string, username string) (*Group, *Response, error) {
// Jira API docs: https://docs.atlassian.com/jira/REST/cloud/#api/2/group-addUserToGroup
func (s *GroupService) AddWithContext(ctx context.Context, groupname string, username string) (*Group, *Response, error) {
apiEndpoint := fmt.Sprintf("/rest/api/2/group/user?groupname=%s", groupname)
var user struct {
Name string `json:"name"`
}
user.Name = username
req, err := s.client.NewRequest("POST", apiEndpoint, &user)
req, err := s.client.NewRequestWithContext(ctx, "POST", apiEndpoint, &user)
if err != nil {
return nil, nil, err
}
@ -136,12 +147,17 @@ func (s *GroupService) Add(groupname string, username string) (*Group, *Response
return responseGroup, resp, nil
}
// Remove removes user from group
// Add wraps AddWithContext using the background context.
func (s *GroupService) Add(groupname string, username string) (*Group, *Response, error) {
return s.AddWithContext(context.Background(), groupname, username)
}
// RemoveWithContext removes user from group
//
// JIRA API docs: https://docs.atlassian.com/jira/REST/cloud/#api/2/group-removeUserFromGroup
func (s *GroupService) Remove(groupname string, username string) (*Response, error) {
// Jira API docs: https://docs.atlassian.com/jira/REST/cloud/#api/2/group-removeUserFromGroup
func (s *GroupService) RemoveWithContext(ctx context.Context, groupname string, username string) (*Response, error) {
apiEndpoint := fmt.Sprintf("/rest/api/2/group/user?groupname=%s&username=%s", groupname, username)
req, err := s.client.NewRequest("DELETE", apiEndpoint, nil)
req, err := s.client.NewRequestWithContext(ctx, "DELETE", apiEndpoint, nil)
if err != nil {
return nil, err
}
@ -154,3 +170,8 @@ func (s *GroupService) Remove(groupname string, username string) (*Response, err
return resp, nil
}
// Remove wraps RemoveWithContext using the background context.
func (s *GroupService) Remove(groupname string, username string) (*Response, error) {
return s.RemoveWithContext(context.Background(), groupname, username)
}

File diff suppressed because it is too large Load Diff

@ -1,24 +1,25 @@
package jira
import (
"context"
"encoding/json"
"fmt"
"io/ioutil"
)
// IssueLinkTypeService handles issue link types for the JIRA instance / API.
// IssueLinkTypeService handles issue link types for the Jira instance / API.
//
// JIRA API docs: https://developer.atlassian.com/cloud/jira/platform/rest/v2/#api-group-Issue-link-types
// Jira API docs: https://developer.atlassian.com/cloud/jira/platform/rest/v2/#api-group-Issue-link-types
type IssueLinkTypeService struct {
client *Client
}
// GetList gets all of the issue link types from JIRA.
// GetListWithContext gets all of the issue link types from Jira.
//
// JIRA API docs: https://developer.atlassian.com/cloud/jira/platform/rest/v2/#api-rest-api-2-issueLinkType-get
func (s *IssueLinkTypeService) GetList() ([]IssueLinkType, *Response, error) {
// Jira API docs: https://developer.atlassian.com/cloud/jira/platform/rest/v2/#api-rest-api-2-issueLinkType-get
func (s *IssueLinkTypeService) GetListWithContext(ctx context.Context) ([]IssueLinkType, *Response, error) {
apiEndpoint := "rest/api/2/issueLinkType"
req, err := s.client.NewRequest("GET", apiEndpoint, nil)
req, err := s.client.NewRequestWithContext(ctx, "GET", apiEndpoint, nil)
if err != nil {
return nil, nil, err
}
@ -31,12 +32,17 @@ func (s *IssueLinkTypeService) GetList() ([]IssueLinkType, *Response, error) {
return linkTypeList, resp, nil
}
// Get gets info of a specific issue link type from JIRA.
// GetList wraps GetListWithContext using the background context.
func (s *IssueLinkTypeService) GetList() ([]IssueLinkType, *Response, error) {
return s.GetListWithContext(context.Background())
}
// GetWithContext gets info of a specific issue link type from Jira.
//
// JIRA API docs: https://developer.atlassian.com/cloud/jira/platform/rest/v2/#api-rest-api-2-issueLinkType-issueLinkTypeId-get
func (s *IssueLinkTypeService) Get(ID string) (*IssueLinkType, *Response, error) {
// Jira API docs: https://developer.atlassian.com/cloud/jira/platform/rest/v2/#api-rest-api-2-issueLinkType-issueLinkTypeId-get
func (s *IssueLinkTypeService) GetWithContext(ctx context.Context, ID string) (*IssueLinkType, *Response, error) {
apiEndPoint := fmt.Sprintf("rest/api/2/issueLinkType/%s", ID)
req, err := s.client.NewRequest("GET", apiEndPoint, nil)
req, err := s.client.NewRequestWithContext(ctx, "GET", apiEndPoint, nil)
if err != nil {
return nil, nil, err
}
@ -49,12 +55,17 @@ func (s *IssueLinkTypeService) Get(ID string) (*IssueLinkType, *Response, error)
return linkType, resp, nil
}
// Create creates an issue link type in JIRA.
// Get wraps GetWithContext using the background context.
func (s *IssueLinkTypeService) Get(ID string) (*IssueLinkType, *Response, error) {
return s.GetWithContext(context.Background(), ID)
}
// CreateWithContext creates an issue link type in Jira.
//
// JIRA API docs: https://developer.atlassian.com/cloud/jira/platform/rest/v2/#api-rest-api-2-issueLinkType-post
func (s *IssueLinkTypeService) Create(linkType *IssueLinkType) (*IssueLinkType, *Response, error) {
// Jira API docs: https://developer.atlassian.com/cloud/jira/platform/rest/v2/#api-rest-api-2-issueLinkType-post
func (s *IssueLinkTypeService) CreateWithContext(ctx context.Context, linkType *IssueLinkType) (*IssueLinkType, *Response, error) {
apiEndpoint := "/rest/api/2/issueLinkType"
req, err := s.client.NewRequest("POST", apiEndpoint, linkType)
req, err := s.client.NewRequestWithContext(ctx, "POST", apiEndpoint, linkType)
if err != nil {
return nil, nil, err
}
@ -68,23 +79,28 @@ func (s *IssueLinkTypeService) Create(linkType *IssueLinkType) (*IssueLinkType,
defer resp.Body.Close()
data, err := ioutil.ReadAll(resp.Body)
if err != nil {
e := fmt.Errorf("Could not read the returned data")
e := fmt.Errorf("could not read the returned data")
return nil, resp, NewJiraError(resp, e)
}
err = json.Unmarshal(data, responseLinkType)
if err != nil {
e := fmt.Errorf("Could no unmarshal the data into struct")
e := fmt.Errorf("could no unmarshal the data into struct")
return nil, resp, NewJiraError(resp, e)
}
return linkType, resp, nil
}
// Update updates an issue link type. The issue is found by key.
// Create wraps CreateWithContext using the background context.
func (s *IssueLinkTypeService) Create(linkType *IssueLinkType) (*IssueLinkType, *Response, error) {
return s.CreateWithContext(context.Background(), linkType)
}
// UpdateWithContext updates an issue link type. The issue is found by key.
//
// JIRA API docs: https://developer.atlassian.com/cloud/jira/platform/rest/v2/#api-rest-api-2-issueLinkType-issueLinkTypeId-put
func (s *IssueLinkTypeService) Update(linkType *IssueLinkType) (*IssueLinkType, *Response, error) {
// Jira API docs: https://developer.atlassian.com/cloud/jira/platform/rest/v2/#api-rest-api-2-issueLinkType-issueLinkTypeId-put
func (s *IssueLinkTypeService) UpdateWithContext(ctx context.Context, linkType *IssueLinkType) (*IssueLinkType, *Response, error) {
apiEndpoint := fmt.Sprintf("rest/api/2/issueLinkType/%s", linkType.ID)
req, err := s.client.NewRequest("PUT", apiEndpoint, linkType)
req, err := s.client.NewRequestWithContext(ctx, "PUT", apiEndpoint, linkType)
if err != nil {
return nil, nil, err
}
@ -96,12 +112,17 @@ func (s *IssueLinkTypeService) Update(linkType *IssueLinkType) (*IssueLinkType,
return &ret, resp, nil
}
// Delete deletes an issue link type based on provided ID.
// Update wraps UpdateWithContext using the background context.
func (s *IssueLinkTypeService) Update(linkType *IssueLinkType) (*IssueLinkType, *Response, error) {
return s.UpdateWithContext(context.Background(), linkType)
}
// DeleteWithContext deletes an issue link type based on provided ID.
//
// JIRA API docs: https://developer.atlassian.com/cloud/jira/platform/rest/v2/#api-rest-api-2-issueLinkType-issueLinkTypeId-delete
func (s *IssueLinkTypeService) Delete(ID string) (*Response, error) {
// Jira API docs: https://developer.atlassian.com/cloud/jira/platform/rest/v2/#api-rest-api-2-issueLinkType-issueLinkTypeId-delete
func (s *IssueLinkTypeService) DeleteWithContext(ctx context.Context, ID string) (*Response, error) {
apiEndpoint := fmt.Sprintf("rest/api/2/issueLinkType/%s", ID)
req, err := s.client.NewRequest("DELETE", apiEndpoint, nil)
req, err := s.client.NewRequestWithContext(ctx, "DELETE", apiEndpoint, nil)
if err != nil {
return nil, err
}
@ -109,3 +130,8 @@ func (s *IssueLinkTypeService) Delete(ID string) (*Response, error) {
resp, err := s.client.Do(req, nil)
return resp, err
}
// Delete wraps DeleteWithContext using the background context.
func (s *IssueLinkTypeService) Delete(ID string) (*Response, error) {
return s.DeleteWithContext(context.Background(), ID)
}

@ -2,6 +2,7 @@ package jira
import (
"bytes"
"context"
"crypto/sha256"
"encoding/hex"
"encoding/json"
@ -14,7 +15,7 @@ import (
"strings"
"time"
"github.com/dgrijalva/jwt-go"
"github.com/golang-jwt/jwt"
"github.com/google/go-querystring/query"
"github.com/pkg/errors"
)
@ -25,7 +26,7 @@ type httpClient interface {
Do(request *http.Request) (response *http.Response, err error)
}
// A Client manages communication with the JIRA API.
// A Client manages communication with the Jira API.
type Client struct {
// HTTP client used to communicate with the API.
client httpClient
@ -36,7 +37,7 @@ type Client struct {
// Session storage if the user authenticates with a Session cookie
session *Session
// Services used for talking to different parts of the JIRA API.
// Services used for talking to different parts of the Jira API.
Authentication *AuthenticationService
Issue *IssueService
Project *ProjectService
@ -55,15 +56,17 @@ type Client struct {
PermissionScheme *PermissionSchemeService
Status *StatusService
IssueLinkType *IssueLinkTypeService
Organization *OrganizationService
ServiceDesk *ServiceDeskService
}
// NewClient returns a new JIRA API client.
// NewClient returns a new Jira API client.
// If a nil httpClient is provided, http.DefaultClient will be used.
// To use API methods which require authentication you can follow the preferred solution and
// provide an http.Client that will perform the authentication for you with OAuth and HTTP Basic (such as that provided by the golang.org/x/oauth2 library).
// As an alternative you can use Session Cookie based authentication provided by this package as well.
// See https://docs.atlassian.com/jira/REST/latest/#authentication
// baseURL is the HTTP endpoint of your JIRA instance and should always be specified with a trailing slash.
// baseURL is the HTTP endpoint of your Jira instance and should always be specified with a trailing slash.
func NewClient(httpClient httpClient, baseURL string) (*Client, error) {
if httpClient == nil {
httpClient = http.DefaultClient
@ -101,14 +104,16 @@ func NewClient(httpClient httpClient, baseURL string) (*Client, error) {
c.PermissionScheme = &PermissionSchemeService{client: c}
c.Status = &StatusService{client: c}
c.IssueLinkType = &IssueLinkTypeService{client: c}
c.Organization = &OrganizationService{client: c}
c.ServiceDesk = &ServiceDeskService{client: c}
return c, nil
}
// NewRawRequest creates an API request.
// NewRawRequestWithContext creates an API request.
// A relative URL can be provided in urlStr, in which case it is resolved relative to the baseURL of the Client.
// Allows using an optional native io.Reader for sourcing the request body.
func (c *Client) NewRawRequest(method, urlStr string, body io.Reader) (*http.Request, error) {
func (c *Client) NewRawRequestWithContext(ctx context.Context, method, urlStr string, body io.Reader) (*http.Request, error) {
rel, err := url.Parse(urlStr)
if err != nil {
return nil, err
@ -118,7 +123,7 @@ func (c *Client) NewRawRequest(method, urlStr string, body io.Reader) (*http.Req
u := c.baseURL.ResolveReference(rel)
req, err := http.NewRequest(method, u.String(), body)
req, err := newRequestWithContext(ctx, method, u.String(), body)
if err != nil {
return nil, err
}
@ -143,10 +148,15 @@ func (c *Client) NewRawRequest(method, urlStr string, body io.Reader) (*http.Req
return req, nil
}
// NewRequest creates an API request.
// NewRawRequest wraps NewRawRequestWithContext using the background context.
func (c *Client) NewRawRequest(method, urlStr string, body io.Reader) (*http.Request, error) {
return c.NewRawRequestWithContext(context.Background(), method, urlStr, body)
}
// NewRequestWithContext creates an API request.
// A relative URL can be provided in urlStr, in which case it is resolved relative to the baseURL of the Client.
// If specified, the value pointed to by body is JSON encoded and included as the request body.
func (c *Client) NewRequest(method, urlStr string, body interface{}) (*http.Request, error) {
func (c *Client) NewRequestWithContext(ctx context.Context, method, urlStr string, body interface{}) (*http.Request, error) {
rel, err := url.Parse(urlStr)
if err != nil {
return nil, err
@ -165,7 +175,7 @@ func (c *Client) NewRequest(method, urlStr string, body interface{}) (*http.Requ
}
}
req, err := http.NewRequest(method, u.String(), buf)
req, err := newRequestWithContext(ctx, method, u.String(), buf)
if err != nil {
return nil, err
}
@ -190,6 +200,11 @@ func (c *Client) NewRequest(method, urlStr string, body interface{}) (*http.Requ
return req, nil
}
// NewRequest wraps NewRequestWithContext using the background context.
func (c *Client) NewRequest(method, urlStr string, body interface{}) (*http.Request, error) {
return c.NewRequestWithContext(context.Background(), method, urlStr, body)
}
// addOptions adds the parameters in opt as URL query parameters to s. opt
// must be a struct whose fields may contain "url" tags.
func addOptions(s string, opt interface{}) (string, error) {
@ -212,10 +227,10 @@ func addOptions(s string, opt interface{}) (string, error) {
return u.String(), nil
}
// NewMultiPartRequest creates an API request including a multi-part file.
// NewMultiPartRequestWithContext creates an API request including a multi-part file.
// A relative URL can be provided in urlStr, in which case it is resolved relative to the baseURL of the Client.
// If specified, the value pointed to by buf is a multipart form.
func (c *Client) NewMultiPartRequest(method, urlStr string, buf *bytes.Buffer) (*http.Request, error) {
func (c *Client) NewMultiPartRequestWithContext(ctx context.Context, method, urlStr string, buf *bytes.Buffer) (*http.Request, error) {
rel, err := url.Parse(urlStr)
if err != nil {
return nil, err
@ -225,7 +240,7 @@ func (c *Client) NewMultiPartRequest(method, urlStr string, buf *bytes.Buffer) (
u := c.baseURL.ResolveReference(rel)
req, err := http.NewRequest(method, u.String(), buf)
req, err := newRequestWithContext(ctx, method, u.String(), buf)
if err != nil {
return nil, err
}
@ -251,6 +266,11 @@ func (c *Client) NewMultiPartRequest(method, urlStr string, buf *bytes.Buffer) (
return req, nil
}
// NewMultiPartRequest wraps NewMultiPartRequestWithContext using the background context.
func (c *Client) NewMultiPartRequest(method, urlStr string, buf *bytes.Buffer) (*http.Request, error) {
return c.NewMultiPartRequestWithContext(context.Background(), method, urlStr, buf)
}
// Do sends an API request and returns the API response.
// The API response is JSON decoded and stored in the value pointed to by v, or returned as an error if an API error has occurred.
func (c *Client) Do(req *http.Request, v interface{}) (*Response, error) {
@ -279,13 +299,13 @@ func (c *Client) Do(req *http.Request, v interface{}) (*Response, error) {
// CheckResponse checks the API response for errors, and returns them if present.
// A response is considered an error if it has a status code outside the 200 range.
// The caller is responsible to analyze the response body.
// The body can contain JSON (if the error is intended) or xml (sometimes JIRA just failes).
// The body can contain JSON (if the error is intended) or xml (sometimes Jira just failes).
func CheckResponse(r *http.Response) error {
if c := r.StatusCode; 200 <= c && c <= 299 {
return nil
}
err := fmt.Errorf("Request failed. Please analyze the request body for more details. Status code: %d", r.StatusCode)
err := fmt.Errorf("request failed. Please analyze the request body for more details. Status code: %d", r.StatusCode)
return err
}
@ -295,7 +315,7 @@ func (c *Client) GetBaseURL() url.URL {
return *c.baseURL
}
// Response represents JIRA API response. It wraps http.Response returned from
// Response represents Jira API response. It wraps http.Response returned from
// API and provides information about paging.
type Response struct {
*http.Response
@ -324,7 +344,6 @@ func (r *Response) populatePageValues(v interface{}) {
r.MaxResults = value.MaxResults
r.Total = value.Total
}
return
}
// BasicAuthTransport is an http.RoundTripper that authenticates all requests
@ -367,9 +386,9 @@ func (t *BasicAuthTransport) transport() http.RoundTripper {
// using Jira's cookie-based authentication.
//
// Note that it is generally preferable to use HTTP BASIC authentication with the REST API.
// However, this resource may be used to mimic the behaviour of JIRA's log-in page (e.g. to display log-in errors to a user).
// However, this resource may be used to mimic the behaviour of Jira's log-in page (e.g. to display log-in errors to a user).
//
// JIRA API docs: https://docs.atlassian.com/jira/REST/latest/#auth/1/session
// Jira API docs: https://docs.atlassian.com/jira/REST/latest/#auth/1/session
type CookieAuthTransport struct {
Username string
Password string
@ -464,7 +483,7 @@ func (t *CookieAuthTransport) transport() http.RoundTripper {
//
// NOTE: this form of auth should be used by add-ons installed from the Atlassian marketplace.
//
// JIRA docs: https://developer.atlassian.com/cloud/jira/platform/understanding-jwt
// Jira docs: https://developer.atlassian.com/cloud/jira/platform/understanding-jwt
// Examples in other languages:
// https://bitbucket.org/atlassian/atlassian-jwt-ruby/src/d44a8e7a4649e4f23edaa784402655fda7c816ea/lib/atlassian/jwt.rb
// https://bitbucket.org/atlassian/atlassian-jwt-py/src/master/atlassian_jwt/url_utils.py

@ -1,6 +1,7 @@
package jira
import (
"context"
"fmt"
"strings"
@ -14,6 +15,11 @@ type CreateMetaInfo struct {
Projects []*MetaProject `json:"projects,omitempty"`
}
// EditMetaInfo contains information about fields and their attributed to edit a ticket.
type EditMetaInfo struct {
Fields tcontainer.MarshalMap `json:"fields,omitempty"`
}
// MetaProject is the meta information about a project returned from createmeta api
type MetaProject struct {
Expand string `json:"expand,omitempty"`
@ -42,16 +48,21 @@ type MetaIssueType struct {
Fields tcontainer.MarshalMap `json:"fields,omitempty"`
}
// GetCreateMeta makes the api call to get the meta information required to create a ticket
func (s *IssueService) GetCreateMeta(projectkeys string) (*CreateMetaInfo, *Response, error) {
return s.GetCreateMetaWithOptions(&GetQueryOptions{ProjectKeys: projectkeys, Expand: "projects.issuetypes.fields"})
// GetCreateMetaWithContext makes the api call to get the meta information required to create a ticket
func (s *IssueService) GetCreateMetaWithContext(ctx context.Context, projectkeys string) (*CreateMetaInfo, *Response, error) {
return s.GetCreateMetaWithOptionsWithContext(ctx, &GetQueryOptions{ProjectKeys: projectkeys, Expand: "projects.issuetypes.fields"})
}
// GetCreateMetaWithOptions makes the api call to get the meta information without requiring to have a projectKey
func (s *IssueService) GetCreateMetaWithOptions(options *GetQueryOptions) (*CreateMetaInfo, *Response, error) {
// GetCreateMeta wraps GetCreateMetaWithContext using the background context.
func (s *IssueService) GetCreateMeta(projectkeys string) (*CreateMetaInfo, *Response, error) {
return s.GetCreateMetaWithContext(context.Background(), projectkeys)
}
// GetCreateMetaWithOptionsWithContext makes the api call to get the meta information without requiring to have a projectKey
func (s *IssueService) GetCreateMetaWithOptionsWithContext(ctx context.Context, options *GetQueryOptions) (*CreateMetaInfo, *Response, error) {
apiEndpoint := "rest/api/2/issue/createmeta"
req, err := s.client.NewRequest("GET", apiEndpoint, nil)
req, err := s.client.NewRequestWithContext(ctx, "GET", apiEndpoint, nil)
if err != nil {
return nil, nil, err
}
@ -73,11 +84,40 @@ func (s *IssueService) GetCreateMetaWithOptions(options *GetQueryOptions) (*Crea
return meta, resp, nil
}
// GetCreateMetaWithOptions wraps GetCreateMetaWithOptionsWithContext using the background context.
func (s *IssueService) GetCreateMetaWithOptions(options *GetQueryOptions) (*CreateMetaInfo, *Response, error) {
return s.GetCreateMetaWithOptionsWithContext(context.Background(), options)
}
// GetEditMetaWithContext makes the api call to get the edit meta information for an issue
func (s *IssueService) GetEditMetaWithContext(ctx context.Context, issue *Issue) (*EditMetaInfo, *Response, error) {
apiEndpoint := fmt.Sprintf("/rest/api/2/issue/%s/editmeta", issue.Key)
req, err := s.client.NewRequestWithContext(ctx, "GET", apiEndpoint, nil)
if err != nil {
return nil, nil, err
}
meta := new(EditMetaInfo)
resp, err := s.client.Do(req, meta)
if err != nil {
return nil, resp, err
}
return meta, resp, nil
}
// GetEditMeta wraps GetEditMetaWithContext using the background context.
func (s *IssueService) GetEditMeta(issue *Issue) (*EditMetaInfo, *Response, error) {
return s.GetEditMetaWithContext(context.Background(), issue)
}
// GetProjectWithName returns a project with "name" from the meta information received. If not found, this returns nil.
// The comparison of the name is case insensitive.
func (m *CreateMetaInfo) GetProjectWithName(name string) *MetaProject {
for _, m := range m.Projects {
if strings.ToLower(m.Name) == strings.ToLower(name) {
if strings.EqualFold(m.Name, name) {
return m
}
}
@ -88,7 +128,7 @@ func (m *CreateMetaInfo) GetProjectWithName(name string) *MetaProject {
// The comparison of the name is case insensitive.
func (m *CreateMetaInfo) GetProjectWithKey(key string) *MetaProject {
for _, m := range m.Projects {
if strings.ToLower(m.Key) == strings.ToLower(key) {
if strings.EqualFold(m.Key, key) {
return m
}
}
@ -99,7 +139,7 @@ func (m *CreateMetaInfo) GetProjectWithKey(key string) *MetaProject {
// The comparison of the name is case insensitive
func (p *MetaProject) GetIssueTypeWithName(name string) *MetaIssueType {
for _, m := range p.IssueTypes {
if strings.ToLower(m.Name) == strings.ToLower(name) {
if strings.EqualFold(m.Name, name) {
return m
}
}
@ -175,7 +215,7 @@ func (t *MetaIssueType) CheckCompleteAndAvailable(config map[string]string) (boo
for name := range mandatory {
requiredFields = append(requiredFields, name)
}
return false, fmt.Errorf("Required field not found in provided jira.fields. Required are: %#v", requiredFields)
return false, fmt.Errorf("required field not found in provided jira.fields. Required are: %#v", requiredFields)
}
}
@ -186,7 +226,7 @@ func (t *MetaIssueType) CheckCompleteAndAvailable(config map[string]string) (boo
for name := range all {
availableFields = append(availableFields, name)
}
return false, fmt.Errorf("Fields in jira.fields are not available in jira. Available are: %#v", availableFields)
return false, fmt.Errorf("fields in jira.fields are not available in jira. Available are: %#v", availableFields)
}
}

387
vendor/github.com/andygrunwald/go-jira/organization.go generated vendored Normal file

@ -0,0 +1,387 @@
package jira
import (
"context"
"fmt"
)
// OrganizationService handles Organizations for the Jira instance / API.
//
// Jira API docs: https://developer.atlassian.com/cloud/jira/service-desk/rest/api-group-organization/
type OrganizationService struct {
client *Client
}
// OrganizationCreationDTO is DTO for creat organization API
type OrganizationCreationDTO struct {
Name string `json:"name,omitempty" structs:"name,omitempty"`
}
// SelfLink Stores REST API URL to the organization.
type SelfLink struct {
Self string `json:"self,omitempty" structs:"self,omitempty"`
}
// Organization contains Organization data
type Organization struct {
ID string `json:"id,omitempty" structs:"id,omitempty"`
Name string `json:"name,omitempty" structs:"name,omitempty"`
Links *SelfLink `json:"_links,omitempty" structs:"_links,omitempty"`
}
// OrganizationUsersDTO contains organization user ids
type OrganizationUsersDTO struct {
AccountIds []string `json:"accountIds,omitempty" structs:"accountIds,omitempty"`
}
// PagedDTO is response of a paged list
type PagedDTO struct {
Size int `json:"size,omitempty" structs:"size,omitempty"`
Start int `json:"start,omitempty" structs:"start,omitempty"`
Limit int `limit:"size,omitempty" structs:"limit,omitempty"`
IsLastPage bool `json:"isLastPage,omitempty" structs:"isLastPage,omitempty"`
Values []interface{} `values:"isLastPage,omitempty" structs:"values,omitempty"`
Expands []string `json:"_expands,omitempty" structs:"_expands,omitempty"`
}
// PropertyKey contains Property key details.
type PropertyKey struct {
Self string `json:"self,omitempty" structs:"self,omitempty"`
Key string `json:"key,omitempty" structs:"key,omitempty"`
}
// PropertyKeys contains an array of PropertyKey
type PropertyKeys struct {
Keys []PropertyKey `json:"keys,omitempty" structs:"keys,omitempty"`
}
// GetAllOrganizationsWithContext returns a list of organizations in
// the Jira Service Management instance.
// Use this method when you want to present a list
// of organizations or want to locate an organization
// by name.
//
// Jira API docs: https://developer.atlassian.com/cloud/jira/service-desk/rest/api-group-organization/#api-group-organization
func (s *OrganizationService) GetAllOrganizationsWithContext(ctx context.Context, start int, limit int, accountID string) (*PagedDTO, *Response, error) {
apiEndPoint := fmt.Sprintf("rest/servicedeskapi/organization?start=%d&limit=%d", start, limit)
if accountID != "" {
apiEndPoint += fmt.Sprintf("&accountId=%s", accountID)
}
req, err := s.client.NewRequestWithContext(ctx, "GET", apiEndPoint, nil)
req.Header.Set("Accept", "application/json")
if err != nil {
return nil, nil, err
}
v := new(PagedDTO)
resp, err := s.client.Do(req, v)
if err != nil {
jerr := NewJiraError(resp, err)
return nil, resp, jerr
}
return v, resp, nil
}
// GetAllOrganizations wraps GetAllOrganizationsWithContext using the background context.
func (s *OrganizationService) GetAllOrganizations(start int, limit int, accountID string) (*PagedDTO, *Response, error) {
return s.GetAllOrganizationsWithContext(context.Background(), start, limit, accountID)
}
// CreateOrganizationWithContext creates an organization by
// passing the name of the organization.
//
// Jira API docs: https://developer.atlassian.com/cloud/jira/service-desk/rest/api-group-organization/#api-rest-servicedeskapi-organization-post
func (s *OrganizationService) CreateOrganizationWithContext(ctx context.Context, name string) (*Organization, *Response, error) {
apiEndPoint := "rest/servicedeskapi/organization"
organization := OrganizationCreationDTO{
Name: name,
}
req, err := s.client.NewRequestWithContext(ctx, "POST", apiEndPoint, organization)
req.Header.Set("Accept", "application/json")
if err != nil {
return nil, nil, err
}
o := new(Organization)
resp, err := s.client.Do(req, &o)
if err != nil {
jerr := NewJiraError(resp, err)
return nil, resp, jerr
}
return o, resp, nil
}
// CreateOrganization wraps CreateOrganizationWithContext using the background context.
func (s *OrganizationService) CreateOrganization(name string) (*Organization, *Response, error) {
return s.CreateOrganizationWithContext(context.Background(), name)
}
// GetOrganizationWithContext returns details of an
// organization. Use this method to get organization
// details whenever your application component is
// passed an organization ID but needs to display
// other organization details.
//
// Jira API docs: https://developer.atlassian.com/cloud/jira/service-desk/rest/api-group-organization/#api-rest-servicedeskapi-organization-organizationid-get
func (s *OrganizationService) GetOrganizationWithContext(ctx context.Context, organizationID int) (*Organization, *Response, error) {
apiEndPoint := fmt.Sprintf("rest/servicedeskapi/organization/%d", organizationID)
req, err := s.client.NewRequestWithContext(ctx, "GET", apiEndPoint, nil)
req.Header.Set("Accept", "application/json")
if err != nil {
return nil, nil, err
}
o := new(Organization)
resp, err := s.client.Do(req, &o)
if err != nil {
jerr := NewJiraError(resp, err)
return nil, resp, jerr
}
return o, resp, nil
}
// GetOrganization wraps GetOrganizationWithContext using the background context.
func (s *OrganizationService) GetOrganization(organizationID int) (*Organization, *Response, error) {
return s.GetOrganizationWithContext(context.Background(), organizationID)
}
// DeleteOrganizationWithContext deletes an organization. Note that
// the organization is deleted regardless
// of other associations it may have.
// For example, associations with service desks.
//
// Jira API docs: https://developer.atlassian.com/cloud/jira/service-desk/rest/api-group-organization/#api-rest-servicedeskapi-organization-organizationid-delete
func (s *OrganizationService) DeleteOrganizationWithContext(ctx context.Context, organizationID int) (*Response, error) {
apiEndPoint := fmt.Sprintf("rest/servicedeskapi/organization/%d", organizationID)
req, err := s.client.NewRequestWithContext(ctx, "DELETE", apiEndPoint, nil)
if err != nil {
return nil, err
}
resp, err := s.client.Do(req, nil)
if err != nil {
jerr := NewJiraError(resp, err)
return resp, jerr
}
return resp, nil
}
// DeleteOrganization wraps DeleteOrganizationWithContext using the background context.
func (s *OrganizationService) DeleteOrganization(organizationID int) (*Response, error) {
return s.DeleteOrganizationWithContext(context.Background(), organizationID)
}
// GetPropertiesKeysWithContext returns the keys of
// all properties for an organization. Use this resource
// when you need to find out what additional properties
// items have been added to an organization.
//
// https://developer.atlassian.com/cloud/jira/service-desk/rest/api-group-organization/#api-rest-servicedeskapi-organization-organizationid-property-get
func (s *OrganizationService) GetPropertiesKeysWithContext(ctx context.Context, organizationID int) (*PropertyKeys, *Response, error) {
apiEndPoint := fmt.Sprintf("rest/servicedeskapi/organization/%d/property", organizationID)
req, err := s.client.NewRequestWithContext(ctx, "GET", apiEndPoint, nil)
req.Header.Set("Accept", "application/json")
if err != nil {
return nil, nil, err
}
pk := new(PropertyKeys)
resp, err := s.client.Do(req, &pk)
if err != nil {
jerr := NewJiraError(resp, err)
return nil, resp, jerr
}
return pk, resp, nil
}
// GetPropertiesKeys wraps GetPropertiesKeysWithContext using the background context.
func (s *OrganizationService) GetPropertiesKeys(organizationID int) (*PropertyKeys, *Response, error) {
return s.GetPropertiesKeysWithContext(context.Background(), organizationID)
}
// GetPropertyWithContext returns the value of a property
// from an organization. Use this method to obtain the JSON
// content for an organization's property.
//
// https://developer.atlassian.com/cloud/jira/service-desk/rest/api-group-organization/#api-rest-servicedeskapi-organization-organizationid-property-propertykey-get
func (s *OrganizationService) GetPropertyWithContext(ctx context.Context, organizationID int, propertyKey string) (*EntityProperty, *Response, error) {
apiEndPoint := fmt.Sprintf("rest/servicedeskapi/organization/%d/property/%s", organizationID, propertyKey)
req, err := s.client.NewRequestWithContext(ctx, "GET", apiEndPoint, nil)
req.Header.Set("Accept", "application/json")
if err != nil {
return nil, nil, err
}
ep := new(EntityProperty)
resp, err := s.client.Do(req, &ep)
if err != nil {
jerr := NewJiraError(resp, err)
return nil, resp, jerr
}
return ep, resp, nil
}
// GetProperty wraps GetPropertyWithContext using the background context.
func (s *OrganizationService) GetProperty(organizationID int, propertyKey string) (*EntityProperty, *Response, error) {
return s.GetPropertyWithContext(context.Background(), organizationID, propertyKey)
}
// SetPropertyWithContext sets the value of a
// property for an organization. Use this
// resource to store custom data against an organization.
//
// https://developer.atlassian.com/cloud/jira/service-desk/rest/api-group-organization/#api-rest-servicedeskapi-organization-organizationid-property-propertykey-put
func (s *OrganizationService) SetPropertyWithContext(ctx context.Context, organizationID int, propertyKey string) (*Response, error) {
apiEndPoint := fmt.Sprintf("rest/servicedeskapi/organization/%d/property/%s", organizationID, propertyKey)
req, err := s.client.NewRequestWithContext(ctx, "PUT", apiEndPoint, nil)
req.Header.Set("Accept", "application/json")
if err != nil {
return nil, err
}
resp, err := s.client.Do(req, nil)
if err != nil {
jerr := NewJiraError(resp, err)
return resp, jerr
}
return resp, nil
}
// SetProperty wraps SetPropertyWithContext using the background context.
func (s *OrganizationService) SetProperty(organizationID int, propertyKey string) (*Response, error) {
return s.SetPropertyWithContext(context.Background(), organizationID, propertyKey)
}
// DeletePropertyWithContext removes a property from an organization.
//
// https://developer.atlassian.com/cloud/jira/service-desk/rest/api-group-organization/#api-rest-servicedeskapi-organization-organizationid-property-propertykey-delete
func (s *OrganizationService) DeletePropertyWithContext(ctx context.Context, organizationID int, propertyKey string) (*Response, error) {
apiEndPoint := fmt.Sprintf("rest/servicedeskapi/organization/%d/property/%s", organizationID, propertyKey)
req, err := s.client.NewRequestWithContext(ctx, "DELETE", apiEndPoint, nil)
req.Header.Set("Accept", "application/json")
if err != nil {
return nil, err
}
resp, err := s.client.Do(req, nil)
if err != nil {
jerr := NewJiraError(resp, err)
return resp, jerr
}
return resp, nil
}
// DeleteProperty wraps DeletePropertyWithContext using the background context.
func (s *OrganizationService) DeleteProperty(organizationID int, propertyKey string) (*Response, error) {
return s.DeletePropertyWithContext(context.Background(), organizationID, propertyKey)
}
// GetUsersWithContext returns all the users
// associated with an organization. Use this
// method where you want to provide a list of
// users for an organization or determine if
// a user is associated with an organization.
//
// https://developer.atlassian.com/cloud/jira/service-desk/rest/api-group-organization/#api-rest-servicedeskapi-organization-organizationid-user-get
func (s *OrganizationService) GetUsersWithContext(ctx context.Context, organizationID int, start int, limit int) (*PagedDTO, *Response, error) {
apiEndPoint := fmt.Sprintf("rest/servicedeskapi/organization/%d/user?start=%d&limit=%d", organizationID, start, limit)
req, err := s.client.NewRequestWithContext(ctx, "GET", apiEndPoint, nil)
req.Header.Set("Accept", "application/json")
if err != nil {
return nil, nil, err
}
users := new(PagedDTO)
resp, err := s.client.Do(req, &users)
if err != nil {
jerr := NewJiraError(resp, err)
return nil, resp, jerr
}
return users, resp, nil
}
// GetUsers wraps GetUsersWithContext using the background context.
func (s *OrganizationService) GetUsers(organizationID int, start int, limit int) (*PagedDTO, *Response, error) {
return s.GetUsersWithContext(context.Background(), organizationID, start, limit)
}
// AddUsersWithContext adds users to an organization.
//
// https://developer.atlassian.com/cloud/jira/service-desk/rest/api-group-organization/#api-rest-servicedeskapi-organization-organizationid-user-post
func (s *OrganizationService) AddUsersWithContext(ctx context.Context, organizationID int, users OrganizationUsersDTO) (*Response, error) {
apiEndPoint := fmt.Sprintf("rest/servicedeskapi/organization/%d/user", organizationID)
req, err := s.client.NewRequestWithContext(ctx, "POST", apiEndPoint, users)
if err != nil {
return nil, err
}
resp, err := s.client.Do(req, nil)
if err != nil {
jerr := NewJiraError(resp, err)
return resp, jerr
}
return resp, nil
}
// AddUsers wraps AddUsersWithContext using the background context.
func (s *OrganizationService) AddUsers(organizationID int, users OrganizationUsersDTO) (*Response, error) {
return s.AddUsersWithContext(context.Background(), organizationID, users)
}
// RemoveUsersWithContext removes users from an organization.
//
// https://developer.atlassian.com/cloud/jira/service-desk/rest/api-group-organization/#api-rest-servicedeskapi-organization-organizationid-user-delete
func (s *OrganizationService) RemoveUsersWithContext(ctx context.Context, organizationID int, users OrganizationUsersDTO) (*Response, error) {
apiEndPoint := fmt.Sprintf("rest/servicedeskapi/organization/%d/user", organizationID)
req, err := s.client.NewRequestWithContext(ctx, "DELETE", apiEndPoint, nil)
req.Header.Set("Accept", "application/json")
if err != nil {
return nil, err
}
resp, err := s.client.Do(req, nil)
if err != nil {
jerr := NewJiraError(resp, err)
return resp, jerr
}
return resp, nil
}
// RemoveUsers wraps RemoveUsersWithContext using the background context.
func (s *OrganizationService) RemoveUsers(organizationID int, users OrganizationUsersDTO) (*Response, error) {
return s.RemoveUsersWithContext(context.Background(), organizationID, users)
}

@ -1,10 +1,13 @@
package jira
import "fmt"
import (
"context"
"fmt"
)
// PermissionSchemeService handles permissionschemes for the JIRA instance / API.
// PermissionSchemeService handles permissionschemes for the Jira instance / API.
//
// JIRA API docs: https://developer.atlassian.com/cloud/jira/platform/rest/v3/#api-group-Permissionscheme
// Jira API docs: https://developer.atlassian.com/cloud/jira/platform/rest/v3/#api-group-Permissionscheme
type PermissionSchemeService struct {
client *Client
}
@ -25,12 +28,12 @@ type Holder struct {
Expand string `json:"expand" structs:"expand"`
}
// GetList returns a list of all permission schemes
// GetListWithContext returns a list of all permission schemes
//
// JIRA API docs: https://developer.atlassian.com/cloud/jira/platform/rest/v3/#api-api-3-permissionscheme-get
func (s *PermissionSchemeService) GetList() (*PermissionSchemes, *Response, error) {
// Jira API docs: https://developer.atlassian.com/cloud/jira/platform/rest/v3/#api-api-3-permissionscheme-get
func (s *PermissionSchemeService) GetListWithContext(ctx context.Context) (*PermissionSchemes, *Response, error) {
apiEndpoint := "/rest/api/3/permissionscheme"
req, err := s.client.NewRequest("GET", apiEndpoint, nil)
req, err := s.client.NewRequestWithContext(ctx, "GET", apiEndpoint, nil)
if err != nil {
return nil, nil, err
}
@ -45,12 +48,17 @@ func (s *PermissionSchemeService) GetList() (*PermissionSchemes, *Response, erro
return pss, resp, nil
}
// Get returns a full representation of the permission scheme for the schemeID
// GetList wraps GetListWithContext using the background context.
func (s *PermissionSchemeService) GetList() (*PermissionSchemes, *Response, error) {
return s.GetListWithContext(context.Background())
}
// GetWithContext returns a full representation of the permission scheme for the schemeID
//
// JIRA API docs: https://developer.atlassian.com/cloud/jira/platform/rest/v3/#api-api-3-permissionscheme-schemeId-get
func (s *PermissionSchemeService) Get(schemeID int) (*PermissionScheme, *Response, error) {
// Jira API docs: https://developer.atlassian.com/cloud/jira/platform/rest/v3/#api-api-3-permissionscheme-schemeId-get
func (s *PermissionSchemeService) GetWithContext(ctx context.Context, schemeID int) (*PermissionScheme, *Response, error) {
apiEndpoint := fmt.Sprintf("/rest/api/3/permissionscheme/%d", schemeID)
req, err := s.client.NewRequest("GET", apiEndpoint, nil)
req, err := s.client.NewRequestWithContext(ctx, "GET", apiEndpoint, nil)
if err != nil {
return nil, nil, err
}
@ -62,8 +70,13 @@ func (s *PermissionSchemeService) Get(schemeID int) (*PermissionScheme, *Respons
return nil, resp, jerr
}
if ps.Self == "" {
return nil, resp, fmt.Errorf("No permissionscheme with ID %d found", schemeID)
return nil, resp, fmt.Errorf("no permissionscheme with ID %d found", schemeID)
}
return ps, resp, nil
}
// Get wraps GetWithContext using the background context.
func (s *PermissionSchemeService) Get(schemeID int) (*PermissionScheme, *Response, error) {
return s.GetWithContext(context.Background(), schemeID)
}

@ -1,13 +1,15 @@
package jira
// PriorityService handles priorities for the JIRA instance / API.
import "context"
// PriorityService handles priorities for the Jira instance / API.
//
// JIRA API docs: https://developer.atlassian.com/cloud/jira/platform/rest/#api-Priority
// Jira API docs: https://developer.atlassian.com/cloud/jira/platform/rest/#api-Priority
type PriorityService struct {
client *Client
}
// Priority represents a priority of a JIRA issue.
// Priority represents a priority of a Jira issue.
// Typical types are "Normal", "Moderate", "Urgent", ...
type Priority struct {
Self string `json:"self,omitempty" structs:"self,omitempty"`
@ -18,12 +20,12 @@ type Priority struct {
Description string `json:"description,omitempty" structs:"description,omitempty"`
}
// GetList gets all priorities from JIRA
// GetListWithContext gets all priorities from Jira
//
// JIRA API docs: https://developer.atlassian.com/cloud/jira/platform/rest/#api-api-2-priority-get
func (s *PriorityService) GetList() ([]Priority, *Response, error) {
// Jira API docs: https://developer.atlassian.com/cloud/jira/platform/rest/#api-api-2-priority-get
func (s *PriorityService) GetListWithContext(ctx context.Context) ([]Priority, *Response, error) {
apiEndpoint := "rest/api/2/priority"
req, err := s.client.NewRequest("GET", apiEndpoint, nil)
req, err := s.client.NewRequestWithContext(ctx, "GET", apiEndpoint, nil)
if err != nil {
return nil, nil, err
}
@ -35,3 +37,8 @@ func (s *PriorityService) GetList() ([]Priority, *Response, error) {
}
return priorityList, resp, nil
}
// GetList wraps GetListWithContext using the background context.
func (s *PriorityService) GetList() ([]Priority, *Response, error) {
return s.GetListWithContext(context.Background())
}

@ -1,14 +1,15 @@
package jira
import (
"context"
"fmt"
"github.com/google/go-querystring/query"
)
// ProjectService handles projects for the JIRA instance / API.
// ProjectService handles projects for the Jira instance / API.
//
// JIRA API docs: https://docs.atlassian.com/jira/REST/latest/#api/2/project
// Jira API docs: https://docs.atlassian.com/jira/REST/latest/#api/2/project
type ProjectService struct {
client *Client
}
@ -34,7 +35,7 @@ type ProjectCategory struct {
Description string `json:"description" structs:"description,omitempty"`
}
// Project represents a JIRA Project.
// Project represents a Jira Project.
type Project struct {
Expand string `json:"expand,omitempty" structs:"expand,omitempty"`
Self string `json:"self,omitempty" structs:"self,omitempty"`
@ -80,20 +81,25 @@ type PermissionScheme struct {
Permissions []Permission `json:"permissions" structs:"permissions,omitempty"`
}
// GetList gets all projects form JIRA
// GetListWithContext gets all projects form Jira
//
// JIRA API docs: https://docs.atlassian.com/jira/REST/latest/#api/2/project-getAllProjects
func (s *ProjectService) GetList() (*ProjectList, *Response, error) {
return s.ListWithOptions(&GetQueryOptions{})
// Jira API docs: https://docs.atlassian.com/jira/REST/latest/#api/2/project-getAllProjects
func (s *ProjectService) GetListWithContext(ctx context.Context) (*ProjectList, *Response, error) {
return s.ListWithOptionsWithContext(ctx, &GetQueryOptions{})
}
// ListWithOptions gets all projects form JIRA with optional query params, like &GetQueryOptions{Expand: "issueTypes"} to get
// GetList wraps GetListWithContext using the background context.
func (s *ProjectService) GetList() (*ProjectList, *Response, error) {
return s.GetListWithContext(context.Background())
}
// ListWithOptionsWithContext gets all projects form Jira with optional query params, like &GetQueryOptions{Expand: "issueTypes"} to get
// a list of all projects and their supported issuetypes
//
// JIRA API docs: https://docs.atlassian.com/jira/REST/latest/#api/2/project-getAllProjects
func (s *ProjectService) ListWithOptions(options *GetQueryOptions) (*ProjectList, *Response, error) {
// Jira API docs: https://docs.atlassian.com/jira/REST/latest/#api/2/project-getAllProjects
func (s *ProjectService) ListWithOptionsWithContext(ctx context.Context, options *GetQueryOptions) (*ProjectList, *Response, error) {
apiEndpoint := "rest/api/2/project"
req, err := s.client.NewRequest("GET", apiEndpoint, nil)
req, err := s.client.NewRequestWithContext(ctx, "GET", apiEndpoint, nil)
if err != nil {
return nil, nil, err
}
@ -116,14 +122,19 @@ func (s *ProjectService) ListWithOptions(options *GetQueryOptions) (*ProjectList
return projectList, resp, nil
}
// Get returns a full representation of the project for the given issue key.
// JIRA will attempt to identify the project by the projectIdOrKey path parameter.
// ListWithOptions wraps ListWithOptionsWithContext using the background context.
func (s *ProjectService) ListWithOptions(options *GetQueryOptions) (*ProjectList, *Response, error) {
return s.ListWithOptionsWithContext(context.Background(), options)
}
// GetWithContext returns a full representation of the project for the given issue key.
// Jira will attempt to identify the project by the projectIdOrKey path parameter.
// This can be an project id, or an project key.
//
// JIRA API docs: https://docs.atlassian.com/jira/REST/latest/#api/2/project-getProject
func (s *ProjectService) Get(projectID string) (*Project, *Response, error) {
// Jira API docs: https://docs.atlassian.com/jira/REST/latest/#api/2/project-getProject
func (s *ProjectService) GetWithContext(ctx context.Context, projectID string) (*Project, *Response, error) {
apiEndpoint := fmt.Sprintf("rest/api/2/project/%s", projectID)
req, err := s.client.NewRequest("GET", apiEndpoint, nil)
req, err := s.client.NewRequestWithContext(ctx, "GET", apiEndpoint, nil)
if err != nil {
return nil, nil, err
}
@ -138,14 +149,19 @@ func (s *ProjectService) Get(projectID string) (*Project, *Response, error) {
return project, resp, nil
}
// GetPermissionScheme returns a full representation of the permission scheme for the project
// JIRA will attempt to identify the project by the projectIdOrKey path parameter.
// Get wraps GetWithContext using the background context.
func (s *ProjectService) Get(projectID string) (*Project, *Response, error) {
return s.GetWithContext(context.Background(), projectID)
}
// GetPermissionSchemeWithContext returns a full representation of the permission scheme for the project
// Jira will attempt to identify the project by the projectIdOrKey path parameter.
// This can be an project id, or an project key.
//
// JIRA API docs: https://docs.atlassian.com/jira/REST/latest/#api/2/project-getProject
func (s *ProjectService) GetPermissionScheme(projectID string) (*PermissionScheme, *Response, error) {
// Jira API docs: https://docs.atlassian.com/jira/REST/latest/#api/2/project-getProject
func (s *ProjectService) GetPermissionSchemeWithContext(ctx context.Context, projectID string) (*PermissionScheme, *Response, error) {
apiEndpoint := fmt.Sprintf("/rest/api/2/project/%s/permissionscheme", projectID)
req, err := s.client.NewRequest("GET", apiEndpoint, nil)
req, err := s.client.NewRequestWithContext(ctx, "GET", apiEndpoint, nil)
if err != nil {
return nil, nil, err
}
@ -159,3 +175,8 @@ func (s *ProjectService) GetPermissionScheme(projectID string) (*PermissionSchem
return ps, resp, nil
}
// GetPermissionScheme wraps GetPermissionSchemeWithContext using the background context.
func (s *ProjectService) GetPermissionScheme(projectID string) (*PermissionScheme, *Response, error) {
return s.GetPermissionSchemeWithContext(context.Background(), projectID)
}

@ -0,0 +1,23 @@
// +build go1.13
// This file provides glue to use Context in `http.Request` with
// Go version 1.13 and higher.
// The function `http.NewRequestWithContext` has been added in Go 1.13.
// Before the release 1.13, to use Context we need creat `http.Request`
// then use the method `WithContext` to create a new `http.Request`
// with Context from the existing `http.Request`.
//
// Doc: https://golang.org/doc/go1.13#net/http
package jira
import (
"context"
"io"
"net/http"
)
func newRequestWithContext(ctx context.Context, method, url string, body io.Reader) (*http.Request, error) {
return http.NewRequestWithContext(ctx, method, url, body)
}

@ -0,0 +1,28 @@
// +build !go1.13
// This file provides glue to use Context in `http.Request` with
// Go version before 1.13.
// The function `http.NewRequestWithContext` has been added in Go 1.13.
// Before the release 1.13, to use Context we need creat `http.Request`
// then use the method `WithContext` to create a new `http.Request`
// with Context from the existing `http.Request`.
//
// Doc: https://golang.org/doc/go1.13#net/http
package jira
import (
"context"
"io"
"net/http"
)
func newRequestWithContext(ctx context.Context, method, url string, body io.Reader) (*http.Request, error) {
r, err := http.NewRequest(method, url, body)
if err != nil {
return nil, err
}
return r.WithContext(ctx), nil
}

@ -1,13 +1,15 @@
package jira
// ResolutionService handles resolutions for the JIRA instance / API.
import "context"
// ResolutionService handles resolutions for the Jira instance / API.
//
// JIRA API docs: https://developer.atlassian.com/cloud/jira/platform/rest/#api-Resolution
// Jira API docs: https://developer.atlassian.com/cloud/jira/platform/rest/#api-Resolution
type ResolutionService struct {
client *Client
}
// Resolution represents a resolution of a JIRA issue.
// Resolution represents a resolution of a Jira issue.
// Typical types are "Fixed", "Suspended", "Won't Fix", ...
type Resolution struct {
Self string `json:"self" structs:"self"`
@ -16,12 +18,12 @@ type Resolution struct {
Name string `json:"name" structs:"name"`
}
// GetList gets all resolutions from JIRA
// GetListWithContext gets all resolutions from Jira
//
// JIRA API docs: https://developer.atlassian.com/cloud/jira/platform/rest/#api-api-2-resolution-get
func (s *ResolutionService) GetList() ([]Resolution, *Response, error) {
// Jira API docs: https://developer.atlassian.com/cloud/jira/platform/rest/#api-api-2-resolution-get
func (s *ResolutionService) GetListWithContext(ctx context.Context) ([]Resolution, *Response, error) {
apiEndpoint := "rest/api/2/resolution"
req, err := s.client.NewRequest("GET", apiEndpoint, nil)
req, err := s.client.NewRequestWithContext(ctx, "GET", apiEndpoint, nil)
if err != nil {
return nil, nil, err
}
@ -33,3 +35,8 @@ func (s *ResolutionService) GetList() ([]Resolution, *Response, error) {
}
return resolutionList, resp, nil
}
// GetList wraps GetListWithContext using the background context.
func (s *ResolutionService) GetList() ([]Resolution, *Response, error) {
return s.GetListWithContext(context.Background())
}

@ -1,17 +1,18 @@
package jira
import (
"context"
"fmt"
)
// RoleService handles roles for the JIRA instance / API.
// RoleService handles roles for the Jira instance / API.
//
// JIRA API docs: https://developer.atlassian.com/cloud/jira/platform/rest/v3/#api-group-Role
// Jira API docs: https://developer.atlassian.com/cloud/jira/platform/rest/v3/#api-group-Role
type RoleService struct {
client *Client
}
// Role represents a JIRA product role
// Role represents a Jira product role
type Role struct {
Self string `json:"self" structs:"self"`
Name string `json:"name" structs:"name"`
@ -20,7 +21,7 @@ type Role struct {
Actors []*Actor `json:"actors" structs:"actors"`
}
// Actor represents a JIRA actor
// Actor represents a Jira actor
type Actor struct {
ID int `json:"id" structs:"id"`
DisplayName string `json:"displayName" structs:"displayName"`
@ -35,12 +36,12 @@ type ActorUser struct {
AccountID string `json:"accountId" structs:"accountId"`
}
// GetList returns a list of all available project roles
// GetListWithContext returns a list of all available project roles
//
// JIRA API docs: https://developer.atlassian.com/cloud/jira/platform/rest/v3/#api-api-3-role-get
func (s *RoleService) GetList() (*[]Role, *Response, error) {
// Jira API docs: https://developer.atlassian.com/cloud/jira/platform/rest/v3/#api-api-3-role-get
func (s *RoleService) GetListWithContext(ctx context.Context) (*[]Role, *Response, error) {
apiEndpoint := "rest/api/3/role"
req, err := s.client.NewRequest("GET", apiEndpoint, nil)
req, err := s.client.NewRequestWithContext(ctx, "GET", apiEndpoint, nil)
if err != nil {
return nil, nil, err
}
@ -53,12 +54,17 @@ func (s *RoleService) GetList() (*[]Role, *Response, error) {
return roles, resp, err
}
// Get retreives a single Role from Jira
// GetList wraps GetListWithContext using the background context.
func (s *RoleService) GetList() (*[]Role, *Response, error) {
return s.GetListWithContext(context.Background())
}
// GetWithContext retreives a single Role from Jira
//
// JIRA API docs: https://developer.atlassian.com/cloud/jira/platform/rest/v3/#api-api-3-role-id-get
func (s *RoleService) Get(roleID int) (*Role, *Response, error) {
// Jira API docs: https://developer.atlassian.com/cloud/jira/platform/rest/v3/#api-api-3-role-id-get
func (s *RoleService) GetWithContext(ctx context.Context, roleID int) (*Role, *Response, error) {
apiEndpoint := fmt.Sprintf("rest/api/3/role/%d", roleID)
req, err := s.client.NewRequest("GET", apiEndpoint, nil)
req, err := s.client.NewRequestWithContext(ctx, "GET", apiEndpoint, nil)
if err != nil {
return nil, nil, err
}
@ -69,8 +75,13 @@ func (s *RoleService) Get(roleID int) (*Role, *Response, error) {
return nil, resp, jerr
}
if role.Self == "" {
return nil, resp, fmt.Errorf("No role with ID %d found", roleID)
return nil, resp, fmt.Errorf("no role with ID %d found", roleID)
}
return role, resp, err
}
// Get wraps GetWithContext using the background context.
func (s *RoleService) Get(roleID int) (*Role, *Response, error) {
return s.GetWithContext(context.Background(), roleID)
}

114
vendor/github.com/andygrunwald/go-jira/servicedesk.go generated vendored Normal file

@ -0,0 +1,114 @@
package jira
import (
"context"
"fmt"
)
// ServiceDeskService handles ServiceDesk for the Jira instance / API.
type ServiceDeskService struct {
client *Client
}
// ServiceDeskOrganizationDTO is a DTO for ServiceDesk organizations
type ServiceDeskOrganizationDTO struct {
OrganizationID int `json:"organizationId,omitempty" structs:"organizationId,omitempty"`
}
// GetOrganizationsWithContext returns a list of
// all organizations associated with a service desk.
//
// https://developer.atlassian.com/cloud/jira/service-desk/rest/api-group-organization/#api-rest-servicedeskapi-servicedesk-servicedeskid-organization-get
func (s *ServiceDeskService) GetOrganizationsWithContext(ctx context.Context, serviceDeskID int, start int, limit int, accountID string) (*PagedDTO, *Response, error) {
apiEndPoint := fmt.Sprintf("rest/servicedeskapi/servicedesk/%d/organization?start=%d&limit=%d", serviceDeskID, start, limit)
if accountID != "" {
apiEndPoint += fmt.Sprintf("&accountId=%s", accountID)
}
req, err := s.client.NewRequestWithContext(ctx, "GET", apiEndPoint, nil)
req.Header.Set("Accept", "application/json")
if err != nil {
return nil, nil, err
}
orgs := new(PagedDTO)
resp, err := s.client.Do(req, &orgs)
if err != nil {
jerr := NewJiraError(resp, err)
return nil, resp, jerr
}
return orgs, resp, nil
}
// GetOrganizations wraps GetOrganizationsWithContext using the background context.
func (s *ServiceDeskService) GetOrganizations(serviceDeskID int, start int, limit int, accountID string) (*PagedDTO, *Response, error) {
return s.GetOrganizationsWithContext(context.Background(), serviceDeskID, start, limit, accountID)
}
// AddOrganizationWithContext adds an organization to
// a service desk. If the organization ID is already
// associated with the service desk, no change is made
// and the resource returns a 204 success code.
//
// https://developer.atlassian.com/cloud/jira/service-desk/rest/api-group-organization/#api-rest-servicedeskapi-servicedesk-servicedeskid-organization-post
func (s *ServiceDeskService) AddOrganizationWithContext(ctx context.Context, serviceDeskID int, organizationID int) (*Response, error) {
apiEndPoint := fmt.Sprintf("rest/servicedeskapi/servicedesk/%d/organization", serviceDeskID)
organization := ServiceDeskOrganizationDTO{
OrganizationID: organizationID,
}
req, err := s.client.NewRequestWithContext(ctx, "POST", apiEndPoint, organization)
if err != nil {
return nil, err
}
resp, err := s.client.Do(req, nil)
if err != nil {
jerr := NewJiraError(resp, err)
return resp, jerr
}
return resp, nil
}
// AddOrganization wraps AddOrganizationWithContext using the background context.
func (s *ServiceDeskService) AddOrganization(serviceDeskID int, organizationID int) (*Response, error) {
return s.AddOrganizationWithContext(context.Background(), serviceDeskID, organizationID)
}
// RemoveOrganizationWithContext removes an organization
// from a service desk. If the organization ID does not
// match an organization associated with the service desk,
// no change is made and the resource returns a 204 success code.
//
// https://developer.atlassian.com/cloud/jira/service-desk/rest/api-group-organization/#api-rest-servicedeskapi-servicedesk-servicedeskid-organization-delete
func (s *ServiceDeskService) RemoveOrganizationWithContext(ctx context.Context, serviceDeskID int, organizationID int) (*Response, error) {
apiEndPoint := fmt.Sprintf("rest/servicedeskapi/servicedesk/%d/organization", serviceDeskID)
organization := ServiceDeskOrganizationDTO{
OrganizationID: organizationID,
}
req, err := s.client.NewRequestWithContext(ctx, "DELETE", apiEndPoint, organization)
if err != nil {
return nil, err
}
resp, err := s.client.Do(req, nil)
if err != nil {
jerr := NewJiraError(resp, err)
return resp, jerr
}
return resp, nil
}
// RemoveOrganization wraps RemoveOrganizationWithContext using the background context.
func (s *ServiceDeskService) RemoveOrganization(serviceDeskID int, organizationID int) (*Response, error) {
return s.RemoveOrganizationWithContext(context.Background(), serviceDeskID, organizationID)
}

@ -1,12 +1,13 @@
package jira
import (
"context"
"fmt"
"github.com/google/go-querystring/query"
)
// SprintService handles sprints in JIRA Agile API.
// SprintService handles sprints in Jira Agile API.
// See https://docs.atlassian.com/jira-software/REST/cloud/
type SprintService struct {
client *Client
@ -22,17 +23,17 @@ type IssuesInSprintResult struct {
Issues []Issue `json:"issues"`
}
// MoveIssuesToSprint moves issues to a sprint, for a given sprint Id.
// MoveIssuesToSprintWithContext moves issues to a sprint, for a given sprint Id.
// Issues can only be moved to open or active sprints.
// The maximum number of issues that can be moved in one operation is 50.
//
// JIRA API docs: https://docs.atlassian.com/jira-software/REST/cloud/#agile/1.0/sprint-moveIssuesToSprint
func (s *SprintService) MoveIssuesToSprint(sprintID int, issueIDs []string) (*Response, error) {
// Jira API docs: https://docs.atlassian.com/jira-software/REST/cloud/#agile/1.0/sprint-moveIssuesToSprint
func (s *SprintService) MoveIssuesToSprintWithContext(ctx context.Context, sprintID int, issueIDs []string) (*Response, error) {
apiEndpoint := fmt.Sprintf("rest/agile/1.0/sprint/%d/issue", sprintID)
payload := IssuesWrapper{Issues: issueIDs}
req, err := s.client.NewRequest("POST", apiEndpoint, payload)
req, err := s.client.NewRequestWithContext(ctx, "POST", apiEndpoint, payload)
if err != nil {
return nil, err
@ -45,15 +46,20 @@ func (s *SprintService) MoveIssuesToSprint(sprintID int, issueIDs []string) (*Re
return resp, err
}
// GetIssuesForSprint returns all issues in a sprint, for a given sprint Id.
// MoveIssuesToSprint wraps MoveIssuesToSprintWithContext using the background context.
func (s *SprintService) MoveIssuesToSprint(sprintID int, issueIDs []string) (*Response, error) {
return s.MoveIssuesToSprintWithContext(context.Background(), sprintID, issueIDs)
}
// GetIssuesForSprintWithContext returns all issues in a sprint, for a given sprint Id.
// This only includes issues that the user has permission to view.
// By default, the returned issues are ordered by rank.
//
// JIRA API Docs: https://docs.atlassian.com/jira-software/REST/cloud/#agile/1.0/sprint-getIssuesForSprint
func (s *SprintService) GetIssuesForSprint(sprintID int) ([]Issue, *Response, error) {
// Jira API Docs: https://docs.atlassian.com/jira-software/REST/cloud/#agile/1.0/sprint-getIssuesForSprint
func (s *SprintService) GetIssuesForSprintWithContext(ctx context.Context, sprintID int) ([]Issue, *Response, error) {
apiEndpoint := fmt.Sprintf("rest/agile/1.0/sprint/%d/issue", sprintID)
req, err := s.client.NewRequest("GET", apiEndpoint, nil)
req, err := s.client.NewRequestWithContext(ctx, "GET", apiEndpoint, nil)
if err != nil {
return nil, nil, err
@ -68,20 +74,25 @@ func (s *SprintService) GetIssuesForSprint(sprintID int) ([]Issue, *Response, er
return result.Issues, resp, err
}
// GetIssue returns a full representation of the issue for the given issue key.
// JIRA will attempt to identify the issue by the issueIdOrKey path parameter.
// GetIssuesForSprint wraps GetIssuesForSprintWithContext using the background context.
func (s *SprintService) GetIssuesForSprint(sprintID int) ([]Issue, *Response, error) {
return s.GetIssuesForSprintWithContext(context.Background(), sprintID)
}
// GetIssueWithContext returns a full representation of the issue for the given issue key.
// Jira will attempt to identify the issue by the issueIdOrKey path parameter.
// This can be an issue id, or an issue key.
// If the issue cannot be found via an exact match, JIRA will also look for the issue in a case-insensitive way, or by looking to see if the issue was moved.
// If the issue cannot be found via an exact match, Jira will also look for the issue in a case-insensitive way, or by looking to see if the issue was moved.
//
// The given options will be appended to the query string
//
// JIRA API docs: https://docs.atlassian.com/jira-software/REST/7.3.1/#agile/1.0/issue-getIssue
// Jira API docs: https://docs.atlassian.com/jira-software/REST/7.3.1/#agile/1.0/issue-getIssue
//
// TODO: create agile service for holding all agile apis' implementation
func (s *SprintService) GetIssue(issueID string, options *GetQueryOptions) (*Issue, *Response, error) {
func (s *SprintService) GetIssueWithContext(ctx context.Context, issueID string, options *GetQueryOptions) (*Issue, *Response, error) {
apiEndpoint := fmt.Sprintf("rest/agile/1.0/issue/%s", issueID)
req, err := s.client.NewRequest("GET", apiEndpoint, nil)
req, err := s.client.NewRequestWithContext(ctx, "GET", apiEndpoint, nil)
if err != nil {
return nil, nil, err
@ -105,3 +116,8 @@ func (s *SprintService) GetIssue(issueID string, options *GetQueryOptions) (*Iss
return issue, resp, nil
}
// GetIssue wraps GetIssueWithContext using the background context.
func (s *SprintService) GetIssue(issueID string, options *GetQueryOptions) (*Issue, *Response, error) {
return s.GetIssueWithContext(context.Background(), issueID, options)
}

@ -1,15 +1,17 @@
package jira
// StatusService handles staties for the JIRA instance / API.
import "context"
// StatusService handles staties for the Jira instance / API.
//
// JIRA API docs: https://developer.atlassian.com/cloud/jira/platform/rest/v2/#api-group-Workflow-statuses
// Jira API docs: https://developer.atlassian.com/cloud/jira/platform/rest/v2/#api-group-Workflow-statuses
type StatusService struct {
client *Client
}
// Status represents the current status of a JIRA issue.
// Status represents the current status of a Jira issue.
// Typical status are "Open", "In Progress", "Closed", ...
// Status can be user defined in every JIRA instance.
// Status can be user defined in every Jira instance.
type Status struct {
Self string `json:"self" structs:"self"`
Description string `json:"description" structs:"description"`
@ -19,12 +21,12 @@ type Status struct {
StatusCategory StatusCategory `json:"statusCategory" structs:"statusCategory"`
}
// GetAllStatuses returns a list of all statuses associated with workflows.
// GetAllStatusesWithContext returns a list of all statuses associated with workflows.
//
// JIRA API docs: https://developer.atlassian.com/cloud/jira/platform/rest/v2/#api-rest-api-2-status-get
func (s *StatusService) GetAllStatuses() ([]Status, *Response, error) {
// Jira API docs: https://developer.atlassian.com/cloud/jira/platform/rest/v2/#api-rest-api-2-status-get
func (s *StatusService) GetAllStatusesWithContext(ctx context.Context) ([]Status, *Response, error) {
apiEndpoint := "rest/api/2/status"
req, err := s.client.NewRequest("GET", apiEndpoint, nil)
req, err := s.client.NewRequestWithContext(ctx, "GET", apiEndpoint, nil)
if err != nil {
return nil, nil, err
@ -38,3 +40,8 @@ func (s *StatusService) GetAllStatuses() ([]Status, *Response, error) {
return statusList, resp, nil
}
// GetAllStatuses wraps GetAllStatusesWithContext using the background context.
func (s *StatusService) GetAllStatuses() ([]Status, *Response, error) {
return s.GetAllStatusesWithContext(context.Background())
}

@ -1,14 +1,16 @@
package jira
// StatusCategoryService handles status categories for the JIRA instance / API.
import "context"
// StatusCategoryService handles status categories for the Jira instance / API.
//
// JIRA API docs: https://developer.atlassian.com/cloud/jira/platform/rest/#api-Statuscategory
// Jira API docs: https://developer.atlassian.com/cloud/jira/platform/rest/#api-Statuscategory
type StatusCategoryService struct {
client *Client
}
// StatusCategory represents the category a status belongs to.
// Those categories can be user defined in every JIRA instance.
// Those categories can be user defined in every Jira instance.
type StatusCategory struct {
Self string `json:"self" structs:"self"`
ID int `json:"id" structs:"id"`
@ -17,7 +19,7 @@ type StatusCategory struct {
ColorName string `json:"colorName" structs:"colorName"`
}
// These constants are the keys of the default JIRA status categories
// These constants are the keys of the default Jira status categories
const (
StatusCategoryComplete = "done"
StatusCategoryInProgress = "indeterminate"
@ -25,12 +27,12 @@ const (
StatusCategoryUndefined = "undefined"
)
// GetList gets all status categories from JIRA
// GetListWithContext gets all status categories from Jira
//
// JIRA API docs: https://developer.atlassian.com/cloud/jira/platform/rest/#api-api-2-statuscategory-get
func (s *StatusCategoryService) GetList() ([]StatusCategory, *Response, error) {
// Jira API docs: https://developer.atlassian.com/cloud/jira/platform/rest/#api-api-2-statuscategory-get
func (s *StatusCategoryService) GetListWithContext(ctx context.Context) ([]StatusCategory, *Response, error) {
apiEndpoint := "rest/api/2/statuscategory"
req, err := s.client.NewRequest("GET", apiEndpoint, nil)
req, err := s.client.NewRequestWithContext(ctx, "GET", apiEndpoint, nil)
if err != nil {
return nil, nil, err
}
@ -42,3 +44,8 @@ func (s *StatusCategoryService) GetList() ([]StatusCategory, *Response, error) {
}
return statusCategoryList, resp, nil
}
// GetList wraps GetListWithContext using the background context.
func (s *StatusCategoryService) GetList() ([]StatusCategory, *Response, error) {
return s.GetListWithContext(context.Background())
}

9
vendor/github.com/andygrunwald/go-jira/types.go generated vendored Normal file

@ -0,0 +1,9 @@
package jira
// Bool is a helper routine that allocates a new bool value
// to store v and returns a pointer to it.
func Bool(v bool) *bool {
p := new(bool)
*p = v
return p
}

@ -1,25 +1,24 @@
package jira
import (
"context"
"encoding/json"
"fmt"
"io/ioutil"
)
// UserService handles users for the JIRA instance / API.
// UserService handles users for the Jira instance / API.
//
// JIRA API docs: https://docs.atlassian.com/jira/REST/cloud/#api/2/user
// Jira API docs: https://developer.atlassian.com/cloud/jira/platform/rest/v2/#api-group-Users
type UserService struct {
client *Client
}
// User represents a JIRA user.
// User represents a Jira user.
type User struct {
Self string `json:"self,omitempty" structs:"self,omitempty"`
AccountID string `json:"accountId,omitempty" structs:"accountId,omitempty"`
AccountType string `json:"accountType,omitempty" structs:"accountType,omitempty"`
// TODO: name & key are deprecated, see:
// https://developer.atlassian.com/cloud/jira/platform/api-changes-for-user-privacy-announcement/
Self string `json:"self,omitempty" structs:"self,omitempty"`
AccountID string `json:"accountId,omitempty" structs:"accountId,omitempty"`
AccountType string `json:"accountType,omitempty" structs:"accountType,omitempty"`
Name string `json:"name,omitempty" structs:"name,omitempty"`
Key string `json:"key,omitempty" structs:"key,omitempty"`
Password string `json:"-"`
@ -47,12 +46,12 @@ type userSearch []userSearchParam
type userSearchF func(userSearch) userSearch
// Get gets user info from JIRA
// GetWithContext gets user info from Jira using its Account Id
//
// JIRA API docs: https://docs.atlassian.com/jira/REST/cloud/#api/2/user-getUser
func (s *UserService) Get(username string) (*User, *Response, error) {
apiEndpoint := fmt.Sprintf("/rest/api/2/user?username=%s", username)
req, err := s.client.NewRequest("GET", apiEndpoint, nil)
// Jira API docs: https://developer.atlassian.com/cloud/jira/platform/rest/v2/#api-rest-api-2-user-get
func (s *UserService) GetWithContext(ctx context.Context, accountId string) (*User, *Response, error) {
apiEndpoint := fmt.Sprintf("/rest/api/2/user?accountId=%s", accountId)
req, err := s.client.NewRequestWithContext(ctx, "GET", apiEndpoint, nil)
if err != nil {
return nil, nil, err
}
@ -65,12 +64,41 @@ func (s *UserService) Get(username string) (*User, *Response, error) {
return user, resp, nil
}
// Create creates an user in JIRA.
// Get wraps GetWithContext using the background context.
func (s *UserService) Get(accountId string) (*User, *Response, error) {
return s.GetWithContext(context.Background(), accountId)
}
// GetByAccountIDWithContext gets user info from Jira
// Searching by another parameter that is not accountId is deprecated,
// but this method is kept for backwards compatibility
// Jira API docs: https://docs.atlassian.com/jira/REST/cloud/#api/2/user-getUser
func (s *UserService) GetByAccountIDWithContext(ctx context.Context, accountID string) (*User, *Response, error) {
apiEndpoint := fmt.Sprintf("/rest/api/2/user?accountId=%s", accountID)
req, err := s.client.NewRequestWithContext(ctx, "GET", apiEndpoint, nil)
if err != nil {
return nil, nil, err
}
user := new(User)
resp, err := s.client.Do(req, user)
if err != nil {
return nil, resp, NewJiraError(resp, err)
}
return user, resp, nil
}
// GetByAccountID wraps GetByAccountIDWithContext using the background context.
func (s *UserService) GetByAccountID(accountID string) (*User, *Response, error) {
return s.GetByAccountIDWithContext(context.Background(), accountID)
}
// CreateWithContext creates an user in Jira.
//
// JIRA API docs: https://docs.atlassian.com/jira/REST/cloud/#api/2/user-createUser
func (s *UserService) Create(user *User) (*User, *Response, error) {
// Jira API docs: https://docs.atlassian.com/jira/REST/cloud/#api/2/user-createUser
func (s *UserService) CreateWithContext(ctx context.Context, user *User) (*User, *Response, error) {
apiEndpoint := "/rest/api/2/user"
req, err := s.client.NewRequest("POST", apiEndpoint, user)
req, err := s.client.NewRequestWithContext(ctx, "POST", apiEndpoint, user)
if err != nil {
return nil, nil, err
}
@ -84,24 +112,29 @@ func (s *UserService) Create(user *User) (*User, *Response, error) {
defer resp.Body.Close()
data, err := ioutil.ReadAll(resp.Body)
if err != nil {
e := fmt.Errorf("Could not read the returned data")
e := fmt.Errorf("could not read the returned data")
return nil, resp, NewJiraError(resp, e)
}
err = json.Unmarshal(data, responseUser)
if err != nil {
e := fmt.Errorf("Could not unmarshall the data into struct")
e := fmt.Errorf("could not unmarshall the data into struct")
return nil, resp, NewJiraError(resp, e)
}
return responseUser, resp, nil
}
// Delete deletes an user from JIRA.
// Create wraps CreateWithContext using the background context.
func (s *UserService) Create(user *User) (*User, *Response, error) {
return s.CreateWithContext(context.Background(), user)
}
// DeleteWithContext deletes an user from Jira.
// Returns http.StatusNoContent on success.
//
// JIRA API docs: https://developer.atlassian.com/cloud/jira/platform/rest/#api-api-2-user-delete
func (s *UserService) Delete(username string) (*Response, error) {
apiEndpoint := fmt.Sprintf("/rest/api/2/user?username=%s", username)
req, err := s.client.NewRequest("DELETE", apiEndpoint, nil)
// Jira API docs: https://developer.atlassian.com/cloud/jira/platform/rest/v2/#api-rest-api-2-user-delete
func (s *UserService) DeleteWithContext(ctx context.Context, accountId string) (*Response, error) {
apiEndpoint := fmt.Sprintf("/rest/api/2/user?accountId=%s", accountId)
req, err := s.client.NewRequestWithContext(ctx, "DELETE", apiEndpoint, nil)
if err != nil {
return nil, err
}
@ -113,12 +146,17 @@ func (s *UserService) Delete(username string) (*Response, error) {
return resp, nil
}
// GetGroups returns the groups which the user belongs to
// Delete wraps DeleteWithContext using the background context.
func (s *UserService) Delete(accountId string) (*Response, error) {
return s.DeleteWithContext(context.Background(), accountId)
}
// GetGroupsWithContext returns the groups which the user belongs to
//
// JIRA API docs: https://docs.atlassian.com/jira/REST/cloud/#api/2/user-getUserGroups
func (s *UserService) GetGroups(username string) (*[]UserGroup, *Response, error) {
apiEndpoint := fmt.Sprintf("/rest/api/2/user/groups?username=%s", username)
req, err := s.client.NewRequest("GET", apiEndpoint, nil)
// Jira API docs: https://developer.atlassian.com/cloud/jira/platform/rest/v2/#api-rest-api-2-user-groups-get
func (s *UserService) GetGroupsWithContext(ctx context.Context, accountId string) (*[]UserGroup, *Response, error) {
apiEndpoint := fmt.Sprintf("/rest/api/2/user/groups?accountId=%s", accountId)
req, err := s.client.NewRequestWithContext(ctx, "GET", apiEndpoint, nil)
if err != nil {
return nil, nil, err
}
@ -131,12 +169,17 @@ func (s *UserService) GetGroups(username string) (*[]UserGroup, *Response, error
return userGroups, resp, nil
}
// Get information about the current logged-in user
// GetGroups wraps GetGroupsWithContext using the background context.
func (s *UserService) GetGroups(accountId string) (*[]UserGroup, *Response, error) {
return s.GetGroupsWithContext(context.Background(), accountId)
}
// GetSelfWithContext information about the current logged-in user
//
// JIRA API docs: https://developer.atlassian.com/cloud/jira/platform/rest/#api-api-2-myself-get
func (s *UserService) GetSelf() (*User, *Response, error) {
// Jira API docs: https://developer.atlassian.com/cloud/jira/platform/rest/v2/#api-rest-api-2-myself-get
func (s *UserService) GetSelfWithContext(ctx context.Context) (*User, *Response, error) {
const apiEndpoint = "rest/api/2/myself"
req, err := s.client.NewRequest("GET", apiEndpoint, nil)
req, err := s.client.NewRequestWithContext(ctx, "GET", apiEndpoint, nil)
if err != nil {
return nil, nil, err
}
@ -148,6 +191,11 @@ func (s *UserService) GetSelf() (*User, *Response, error) {
return &user, resp, nil
}
// GetSelf wraps GetSelfWithContext using the background context.
func (s *UserService) GetSelf() (*User, *Response, error) {
return s.GetSelfWithContext(context.Background())
}
// WithMaxResults sets the max results to return
func WithMaxResults(maxResults int) userSearchF {
return func(s userSearch) userSearch {
@ -180,14 +228,14 @@ func WithInactive(inactive bool) userSearchF {
}
}
// Find searches for user info from JIRA:
// It can find users by email, username or name
// FindWithContext searches for user info from Jira:
// It can find users by email or display name using the query parameter
//
// JIRA API docs: https://docs.atlassian.com/jira/REST/cloud/#api/2/user-findUsers
func (s *UserService) Find(property string, tweaks ...userSearchF) ([]User, *Response, error) {
// Jira API docs: https://developer.atlassian.com/cloud/jira/platform/rest/v2/#api-rest-api-2-user-search-get
func (s *UserService) FindWithContext(ctx context.Context, property string, tweaks ...userSearchF) ([]User, *Response, error) {
search := []userSearchParam{
{
name: "username",
name: "query",
value: property,
},
}
@ -201,7 +249,7 @@ func (s *UserService) Find(property string, tweaks ...userSearchF) ([]User, *Res
}
apiEndpoint := fmt.Sprintf("/rest/api/2/user/search?%s", queryString[:len(queryString)-1])
req, err := s.client.NewRequest("GET", apiEndpoint, nil)
req, err := s.client.NewRequestWithContext(ctx, "GET", apiEndpoint, nil)
if err != nil {
return nil, nil, err
}
@ -213,3 +261,8 @@ func (s *UserService) Find(property string, tweaks ...userSearchF) ([]User, *Res
}
return users, resp, nil
}
// Find wraps FindWithContext using the background context.
func (s *UserService) Find(property string, tweaks ...userSearchF) ([]User, *Response, error) {
return s.FindWithContext(context.Background(), property, tweaks...)
}

Some files were not shown because too many files have changed in this diff Show More