mirror of
https://github.com/letic/terraform-provider-google.git
synced 2024-07-08 19:18:30 +00:00
More Cloudbuild Trigger Step support (#3346)
<!-- This change is generated by MagicModules. --> /cc @chrisst
This commit is contained in:
parent
6121d539c2
commit
d1dbdb0252
|
@ -68,10 +68,67 @@ func resourceCloudBuildTrigger() *schema.Resource {
|
||||||
Type: schema.TypeString,
|
Type: schema.TypeString,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
"dir": {
|
||||||
|
Type: schema.TypeString,
|
||||||
|
Optional: true,
|
||||||
|
},
|
||||||
|
"entrypoint": {
|
||||||
|
Type: schema.TypeString,
|
||||||
|
Optional: true,
|
||||||
|
},
|
||||||
|
"env": {
|
||||||
|
Type: schema.TypeList,
|
||||||
|
Optional: true,
|
||||||
|
Elem: &schema.Schema{
|
||||||
|
Type: schema.TypeString,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
"id": {
|
||||||
|
Type: schema.TypeString,
|
||||||
|
Optional: true,
|
||||||
|
},
|
||||||
"name": {
|
"name": {
|
||||||
Type: schema.TypeString,
|
Type: schema.TypeString,
|
||||||
Optional: true,
|
Optional: true,
|
||||||
},
|
},
|
||||||
|
"secret_env": {
|
||||||
|
Type: schema.TypeList,
|
||||||
|
Optional: true,
|
||||||
|
Elem: &schema.Schema{
|
||||||
|
Type: schema.TypeString,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
"timeout": {
|
||||||
|
Type: schema.TypeString,
|
||||||
|
Optional: true,
|
||||||
|
},
|
||||||
|
"timing": {
|
||||||
|
Type: schema.TypeString,
|
||||||
|
Optional: true,
|
||||||
|
},
|
||||||
|
"volumes": {
|
||||||
|
Type: schema.TypeList,
|
||||||
|
Optional: true,
|
||||||
|
Elem: &schema.Resource{
|
||||||
|
Schema: map[string]*schema.Schema{
|
||||||
|
"name": {
|
||||||
|
Type: schema.TypeString,
|
||||||
|
Optional: true,
|
||||||
|
},
|
||||||
|
"path": {
|
||||||
|
Type: schema.TypeString,
|
||||||
|
Optional: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
"wait_for": {
|
||||||
|
Type: schema.TypeList,
|
||||||
|
Optional: true,
|
||||||
|
Elem: &schema.Schema{
|
||||||
|
Type: schema.TypeString,
|
||||||
|
},
|
||||||
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
@ -540,6 +597,15 @@ func flattenCloudBuildTriggerBuildStep(v interface{}, d *schema.ResourceData) in
|
||||||
transformed = append(transformed, map[string]interface{}{
|
transformed = append(transformed, map[string]interface{}{
|
||||||
"name": flattenCloudBuildTriggerBuildStepName(original["name"], d),
|
"name": flattenCloudBuildTriggerBuildStepName(original["name"], d),
|
||||||
"args": flattenCloudBuildTriggerBuildStepArgs(original["args"], d),
|
"args": flattenCloudBuildTriggerBuildStepArgs(original["args"], d),
|
||||||
|
"env": flattenCloudBuildTriggerBuildStepEnv(original["env"], d),
|
||||||
|
"id": flattenCloudBuildTriggerBuildStepId(original["id"], d),
|
||||||
|
"entrypoint": flattenCloudBuildTriggerBuildStepEntrypoint(original["entrypoint"], d),
|
||||||
|
"dir": flattenCloudBuildTriggerBuildStepDir(original["dir"], d),
|
||||||
|
"secret_env": flattenCloudBuildTriggerBuildStepSecretEnv(original["secretEnv"], d),
|
||||||
|
"timeout": flattenCloudBuildTriggerBuildStepTimeout(original["timeout"], d),
|
||||||
|
"timing": flattenCloudBuildTriggerBuildStepTiming(original["timing"], d),
|
||||||
|
"volumes": flattenCloudBuildTriggerBuildStepVolumes(original["volumes"], d),
|
||||||
|
"wait_for": flattenCloudBuildTriggerBuildStepWaitFor(original["waitFor"], d),
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
return transformed
|
return transformed
|
||||||
|
@ -552,6 +618,65 @@ func flattenCloudBuildTriggerBuildStepArgs(v interface{}, d *schema.ResourceData
|
||||||
return v
|
return v
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func flattenCloudBuildTriggerBuildStepEnv(v interface{}, d *schema.ResourceData) interface{} {
|
||||||
|
return v
|
||||||
|
}
|
||||||
|
|
||||||
|
func flattenCloudBuildTriggerBuildStepId(v interface{}, d *schema.ResourceData) interface{} {
|
||||||
|
return v
|
||||||
|
}
|
||||||
|
|
||||||
|
func flattenCloudBuildTriggerBuildStepEntrypoint(v interface{}, d *schema.ResourceData) interface{} {
|
||||||
|
return v
|
||||||
|
}
|
||||||
|
|
||||||
|
func flattenCloudBuildTriggerBuildStepDir(v interface{}, d *schema.ResourceData) interface{} {
|
||||||
|
return v
|
||||||
|
}
|
||||||
|
|
||||||
|
func flattenCloudBuildTriggerBuildStepSecretEnv(v interface{}, d *schema.ResourceData) interface{} {
|
||||||
|
return v
|
||||||
|
}
|
||||||
|
|
||||||
|
func flattenCloudBuildTriggerBuildStepTimeout(v interface{}, d *schema.ResourceData) interface{} {
|
||||||
|
return v
|
||||||
|
}
|
||||||
|
|
||||||
|
func flattenCloudBuildTriggerBuildStepTiming(v interface{}, d *schema.ResourceData) interface{} {
|
||||||
|
return v
|
||||||
|
}
|
||||||
|
|
||||||
|
func flattenCloudBuildTriggerBuildStepVolumes(v interface{}, d *schema.ResourceData) interface{} {
|
||||||
|
if v == nil {
|
||||||
|
return v
|
||||||
|
}
|
||||||
|
l := v.([]interface{})
|
||||||
|
transformed := make([]interface{}, 0, len(l))
|
||||||
|
for _, raw := range l {
|
||||||
|
original := raw.(map[string]interface{})
|
||||||
|
if len(original) < 1 {
|
||||||
|
// Do not include empty json objects coming back from the api
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
transformed = append(transformed, map[string]interface{}{
|
||||||
|
"name": flattenCloudBuildTriggerBuildStepVolumesName(original["name"], d),
|
||||||
|
"path": flattenCloudBuildTriggerBuildStepVolumesPath(original["path"], d),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
return transformed
|
||||||
|
}
|
||||||
|
func flattenCloudBuildTriggerBuildStepVolumesName(v interface{}, d *schema.ResourceData) interface{} {
|
||||||
|
return v
|
||||||
|
}
|
||||||
|
|
||||||
|
func flattenCloudBuildTriggerBuildStepVolumesPath(v interface{}, d *schema.ResourceData) interface{} {
|
||||||
|
return v
|
||||||
|
}
|
||||||
|
|
||||||
|
func flattenCloudBuildTriggerBuildStepWaitFor(v interface{}, d *schema.ResourceData) interface{} {
|
||||||
|
return v
|
||||||
|
}
|
||||||
|
|
||||||
func expandCloudBuildTriggerDescription(v interface{}, d TerraformResourceData, config *Config) (interface{}, error) {
|
func expandCloudBuildTriggerDescription(v interface{}, d TerraformResourceData, config *Config) (interface{}, error) {
|
||||||
return v, nil
|
return v, nil
|
||||||
}
|
}
|
||||||
|
@ -726,6 +851,69 @@ func expandCloudBuildTriggerBuildStep(v interface{}, d TerraformResourceData, co
|
||||||
transformed["args"] = transformedArgs
|
transformed["args"] = transformedArgs
|
||||||
}
|
}
|
||||||
|
|
||||||
|
transformedEnv, err := expandCloudBuildTriggerBuildStepEnv(original["env"], d, config)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
} else if val := reflect.ValueOf(transformedEnv); val.IsValid() && !isEmptyValue(val) {
|
||||||
|
transformed["env"] = transformedEnv
|
||||||
|
}
|
||||||
|
|
||||||
|
transformedId, err := expandCloudBuildTriggerBuildStepId(original["id"], d, config)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
} else if val := reflect.ValueOf(transformedId); val.IsValid() && !isEmptyValue(val) {
|
||||||
|
transformed["id"] = transformedId
|
||||||
|
}
|
||||||
|
|
||||||
|
transformedEntrypoint, err := expandCloudBuildTriggerBuildStepEntrypoint(original["entrypoint"], d, config)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
} else if val := reflect.ValueOf(transformedEntrypoint); val.IsValid() && !isEmptyValue(val) {
|
||||||
|
transformed["entrypoint"] = transformedEntrypoint
|
||||||
|
}
|
||||||
|
|
||||||
|
transformedDir, err := expandCloudBuildTriggerBuildStepDir(original["dir"], d, config)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
} else if val := reflect.ValueOf(transformedDir); val.IsValid() && !isEmptyValue(val) {
|
||||||
|
transformed["dir"] = transformedDir
|
||||||
|
}
|
||||||
|
|
||||||
|
transformedSecretEnv, err := expandCloudBuildTriggerBuildStepSecretEnv(original["secret_env"], d, config)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
} else if val := reflect.ValueOf(transformedSecretEnv); val.IsValid() && !isEmptyValue(val) {
|
||||||
|
transformed["secretEnv"] = transformedSecretEnv
|
||||||
|
}
|
||||||
|
|
||||||
|
transformedTimeout, err := expandCloudBuildTriggerBuildStepTimeout(original["timeout"], d, config)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
} else if val := reflect.ValueOf(transformedTimeout); val.IsValid() && !isEmptyValue(val) {
|
||||||
|
transformed["timeout"] = transformedTimeout
|
||||||
|
}
|
||||||
|
|
||||||
|
transformedTiming, err := expandCloudBuildTriggerBuildStepTiming(original["timing"], d, config)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
} else if val := reflect.ValueOf(transformedTiming); val.IsValid() && !isEmptyValue(val) {
|
||||||
|
transformed["timing"] = transformedTiming
|
||||||
|
}
|
||||||
|
|
||||||
|
transformedVolumes, err := expandCloudBuildTriggerBuildStepVolumes(original["volumes"], d, config)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
} else if val := reflect.ValueOf(transformedVolumes); val.IsValid() && !isEmptyValue(val) {
|
||||||
|
transformed["volumes"] = transformedVolumes
|
||||||
|
}
|
||||||
|
|
||||||
|
transformedWaitFor, err := expandCloudBuildTriggerBuildStepWaitFor(original["wait_for"], d, config)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
} else if val := reflect.ValueOf(transformedWaitFor); val.IsValid() && !isEmptyValue(val) {
|
||||||
|
transformed["waitFor"] = transformedWaitFor
|
||||||
|
}
|
||||||
|
|
||||||
req = append(req, transformed)
|
req = append(req, transformed)
|
||||||
}
|
}
|
||||||
return req, nil
|
return req, nil
|
||||||
|
@ -738,3 +926,72 @@ func expandCloudBuildTriggerBuildStepName(v interface{}, d TerraformResourceData
|
||||||
func expandCloudBuildTriggerBuildStepArgs(v interface{}, d TerraformResourceData, config *Config) (interface{}, error) {
|
func expandCloudBuildTriggerBuildStepArgs(v interface{}, d TerraformResourceData, config *Config) (interface{}, error) {
|
||||||
return v, nil
|
return v, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func expandCloudBuildTriggerBuildStepEnv(v interface{}, d TerraformResourceData, config *Config) (interface{}, error) {
|
||||||
|
return v, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func expandCloudBuildTriggerBuildStepId(v interface{}, d TerraformResourceData, config *Config) (interface{}, error) {
|
||||||
|
return v, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func expandCloudBuildTriggerBuildStepEntrypoint(v interface{}, d TerraformResourceData, config *Config) (interface{}, error) {
|
||||||
|
return v, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func expandCloudBuildTriggerBuildStepDir(v interface{}, d TerraformResourceData, config *Config) (interface{}, error) {
|
||||||
|
return v, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func expandCloudBuildTriggerBuildStepSecretEnv(v interface{}, d TerraformResourceData, config *Config) (interface{}, error) {
|
||||||
|
return v, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func expandCloudBuildTriggerBuildStepTimeout(v interface{}, d TerraformResourceData, config *Config) (interface{}, error) {
|
||||||
|
return v, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func expandCloudBuildTriggerBuildStepTiming(v interface{}, d TerraformResourceData, config *Config) (interface{}, error) {
|
||||||
|
return v, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func expandCloudBuildTriggerBuildStepVolumes(v interface{}, d TerraformResourceData, config *Config) (interface{}, error) {
|
||||||
|
l := v.([]interface{})
|
||||||
|
req := make([]interface{}, 0, len(l))
|
||||||
|
for _, raw := range l {
|
||||||
|
if raw == nil {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
original := raw.(map[string]interface{})
|
||||||
|
transformed := make(map[string]interface{})
|
||||||
|
|
||||||
|
transformedName, err := expandCloudBuildTriggerBuildStepVolumesName(original["name"], d, config)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
} else if val := reflect.ValueOf(transformedName); val.IsValid() && !isEmptyValue(val) {
|
||||||
|
transformed["name"] = transformedName
|
||||||
|
}
|
||||||
|
|
||||||
|
transformedPath, err := expandCloudBuildTriggerBuildStepVolumesPath(original["path"], d, config)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
} else if val := reflect.ValueOf(transformedPath); val.IsValid() && !isEmptyValue(val) {
|
||||||
|
transformed["path"] = transformedPath
|
||||||
|
}
|
||||||
|
|
||||||
|
req = append(req, transformed)
|
||||||
|
}
|
||||||
|
return req, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func expandCloudBuildTriggerBuildStepVolumesName(v interface{}, d TerraformResourceData, config *Config) (interface{}, error) {
|
||||||
|
return v, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func expandCloudBuildTriggerBuildStepVolumesPath(v interface{}, d TerraformResourceData, config *Config) (interface{}, error) {
|
||||||
|
return v, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func expandCloudBuildTriggerBuildStepWaitFor(v interface{}, d TerraformResourceData, config *Config) (interface{}, error) {
|
||||||
|
return v, nil
|
||||||
|
}
|
||||||
|
|
|
@ -35,6 +35,26 @@ func TestAccCloudBuildTrigger_basic(t *testing.T) {
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestAccCloudBuildTrigger_fullStep(t *testing.T) {
|
||||||
|
t.Parallel()
|
||||||
|
|
||||||
|
resource.Test(t, resource.TestCase{
|
||||||
|
PreCheck: func() { testAccPreCheck(t) },
|
||||||
|
Providers: testAccProviders,
|
||||||
|
CheckDestroy: testAccCheckCloudBuildTriggerDestroy,
|
||||||
|
Steps: []resource.TestStep{
|
||||||
|
{
|
||||||
|
Config: testAccCloudBuildTrigger_fullStep(),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
ResourceName: "google_cloudbuild_trigger.build_trigger",
|
||||||
|
ImportState: true,
|
||||||
|
ImportStateVerify: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
func testGoogleCloudBuildTrigger_basic() string {
|
func testGoogleCloudBuildTrigger_basic() string {
|
||||||
return fmt.Sprintf(`
|
return fmt.Sprintf(`
|
||||||
resource "google_cloudbuild_trigger" "build_trigger" {
|
resource "google_cloudbuild_trigger" "build_trigger" {
|
||||||
|
@ -53,6 +73,7 @@ resource "google_cloudbuild_trigger" "build_trigger" {
|
||||||
step {
|
step {
|
||||||
name = "gcr.io/cloud-builders/go"
|
name = "gcr.io/cloud-builders/go"
|
||||||
args = ["build", "my_package"]
|
args = ["build", "my_package"]
|
||||||
|
env = ["env1=two"]
|
||||||
}
|
}
|
||||||
step {
|
step {
|
||||||
name = "gcr.io/cloud-builders/docker"
|
name = "gcr.io/cloud-builders/docker"
|
||||||
|
@ -63,6 +84,32 @@ resource "google_cloudbuild_trigger" "build_trigger" {
|
||||||
`)
|
`)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func testAccCloudBuildTrigger_fullStep() string {
|
||||||
|
return fmt.Sprintf(`
|
||||||
|
resource "google_cloudbuild_trigger" "build_trigger" {
|
||||||
|
description = "acceptance test build trigger"
|
||||||
|
trigger_template {
|
||||||
|
branch_name = "master"
|
||||||
|
repo_name = "some-repo"
|
||||||
|
}
|
||||||
|
build {
|
||||||
|
images = ["gcr.io/$PROJECT_ID/$REPO_NAME:$COMMIT_SHA"]
|
||||||
|
tags = ["team-a", "service-b"]
|
||||||
|
step {
|
||||||
|
name = "gcr.io/cloud-builders/go"
|
||||||
|
args = ["build", "my_package"]
|
||||||
|
env = ["env1=two"]
|
||||||
|
dir = "directory"
|
||||||
|
id = "12345"
|
||||||
|
secret_env = ["fooo"]
|
||||||
|
timeout = "100s"
|
||||||
|
wait_for = ["something"]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
`)
|
||||||
|
}
|
||||||
|
|
||||||
func testGoogleCloudBuildTrigger_updated() string {
|
func testGoogleCloudBuildTrigger_updated() string {
|
||||||
return fmt.Sprintf(`
|
return fmt.Sprintf(`
|
||||||
resource "google_cloudbuild_trigger" "build_trigger" {
|
resource "google_cloudbuild_trigger" "build_trigger" {
|
||||||
|
|
|
@ -188,6 +188,87 @@ The `step` block supports:
|
||||||
entrypoint, the first element in args is used as the entrypoint, and the
|
entrypoint, the first element in args is used as the entrypoint, and the
|
||||||
remainder will be used as arguments.
|
remainder will be used as arguments.
|
||||||
|
|
||||||
|
* `env` -
|
||||||
|
(Optional)
|
||||||
|
A list of environment variable definitions to be used when
|
||||||
|
running a step.
|
||||||
|
The elements are of the form "KEY=VALUE" for the environment variable
|
||||||
|
"KEY" being given the value "VALUE".
|
||||||
|
|
||||||
|
* `id` -
|
||||||
|
(Optional)
|
||||||
|
Unique identifier for this build step, used in `wait_for` to
|
||||||
|
reference this build step as a dependency.
|
||||||
|
|
||||||
|
* `entrypoint` -
|
||||||
|
(Optional)
|
||||||
|
Entrypoint to be used instead of the build step image's
|
||||||
|
default entrypoint.
|
||||||
|
If unset, the image's default entrypoint is used
|
||||||
|
|
||||||
|
* `dir` -
|
||||||
|
(Optional)
|
||||||
|
Working directory to use when running this step's container.
|
||||||
|
If this value is a relative path, it is relative to the build's working
|
||||||
|
directory. If this value is absolute, it may be outside the build's working
|
||||||
|
directory, in which case the contents of the path may not be persisted
|
||||||
|
across build step executions, unless a `volume` for that path is specified.
|
||||||
|
If the build specifies a `RepoSource` with `dir` and a step with a
|
||||||
|
`dir`,
|
||||||
|
which specifies an absolute path, the `RepoSource` `dir` is ignored
|
||||||
|
for the step's execution.
|
||||||
|
|
||||||
|
* `secret_env` -
|
||||||
|
(Optional)
|
||||||
|
A list of environment variables which are encrypted using
|
||||||
|
a Cloud Key
|
||||||
|
Management Service crypto key. These values must be specified in
|
||||||
|
the build's `Secret`.
|
||||||
|
|
||||||
|
* `timeout` -
|
||||||
|
(Optional)
|
||||||
|
Time limit for executing this build step. If not defined,
|
||||||
|
the step has no
|
||||||
|
time limit and will be allowed to continue to run until either it
|
||||||
|
completes or the build itself times out.
|
||||||
|
|
||||||
|
* `timing` -
|
||||||
|
(Optional)
|
||||||
|
Output only. Stores timing information for executing this
|
||||||
|
build step.
|
||||||
|
|
||||||
|
* `volumes` -
|
||||||
|
(Optional)
|
||||||
|
List of volumes to mount into the build step.
|
||||||
|
Each volume is created as an empty volume prior to execution of the
|
||||||
|
build step. Upon completion of the build, volumes and their contents
|
||||||
|
are discarded.
|
||||||
|
Using a named volume in only one step is not valid as it is
|
||||||
|
indicative of a build request with an incorrect configuration. Structure is documented below.
|
||||||
|
|
||||||
|
* `wait_for` -
|
||||||
|
(Optional)
|
||||||
|
The ID(s) of the step(s) that this build step depends on.
|
||||||
|
This build step will not start until all the build steps in `wait_for`
|
||||||
|
have completed successfully. If `wait_for` is empty, this build step
|
||||||
|
will start when all previous build steps in the `Build.Steps` list
|
||||||
|
have completed successfully.
|
||||||
|
|
||||||
|
|
||||||
|
The `volumes` block supports:
|
||||||
|
|
||||||
|
* `name` -
|
||||||
|
(Optional)
|
||||||
|
Name of the volume to mount.
|
||||||
|
Volume names must be unique per build step and must be valid names for
|
||||||
|
Docker volumes. Each named volume must be used by at least two build steps.
|
||||||
|
|
||||||
|
* `path` -
|
||||||
|
(Optional)
|
||||||
|
Path at which to mount the volume.
|
||||||
|
Paths must be absolute and cannot conflict with other volume paths on
|
||||||
|
the same build step or with certain reserved volume paths.
|
||||||
|
|
||||||
## Attributes Reference
|
## Attributes Reference
|
||||||
|
|
||||||
In addition to the arguments listed above, the following computed attributes are exported:
|
In addition to the arguments listed above, the following computed attributes are exported:
|
||||||
|
|
Loading…
Reference in New Issue
Block a user