DEV Community

Cover image for Day 69: Two Plugins and One Setting
Nnamdi Felix Ibe
Nnamdi Felix Ibe

Posted on AI-assisted

Day 69: Two Plugins and One Setting

Day 69 of DevOps, Day 19 of Azure. Both tasks today were about reaching for the narrow instrument when a broader one was sitting right next to it. Two named Jenkins plugins rather than whatever else looked useful, and one container closed rather than the whole storage account.

The broader tool would have passed both tasks. That is what makes it worth writing down.

One Jenkins task, one Azure task. Install the Git and GitLab plugins, then convert a public blob container to private without touching its neighbour. The tasks come from the KodeKloud Engineer platform.

Task 1: Install two plugins

The task: log in as admin, install the Git and GitLab plugins, restart Jenkins.

There are no commands in this one. Manage Jenkins, then Plugins, then the Available tab, search for each, tick, install, restart. Which makes the interesting part everything the four clicks did on my behalf.

What ticking two boxes actually downloaded

The progress list that follows is much longer than two entries, and the docs explain why: "Plugins can be automatically downloaded, with their dependencies, from the Update Centre."

Git pulls in the Git client plugin, SCM API and several others. GitLab depends on Git, so the order I picked them in did not matter. It would matter if a dependency were unavailable, because the Update Centre resolves against what it is offering now rather than against what is already installed.

That is the first reason to install two named plugins rather than a handful that look handy. Every plugin is code running with the controller's privileges, and the dependency tree means you install more of it than you chose.

The restart checkbox, and a sourcing problem

The screen offers "Restart Jenkins when installation is complete and no jobs are running", and I ticked it.

Going to cite that, I could not. The string is on the screen and is not in Jenkins' documentation anywhere. The labels the docs do use are "Install without restart" and "Download now and install after restart". So the checkbox is real, and quoting it as documentation would be putting words in Jenkins' mouth.

A restart is not always required either. The docs: "Most plugins can be installed and used immediately by checking the box adjacent to the plugin." Where it is genuinely required is the manual route, dropping a .hpi into JENKINS_HOME/plugins, after which "The controller must be restarted before the plugin is loaded and made available in the Jenkins environment."

The useful half of that checkbox is the second clause. A restart during a build loses the build. On a controller with anything scheduled, install without restart and take the restart in a quiet window.

What the docs recommend

Make the plugin set reproducible. Clicking works once; it does not survive a rebuild. Jenkins documents a CLI route:

java -jar jenkins-cli.jar -s http://localhost:8080/ install-plugin SOURCE ... [-deploy] [-name VAL] [-restart]
Enter fullscreen mode Exit fullscreen mode

described as "Installs a plugin either from a file, an URL, or from the update centre", with -restart to "Restart Jenkins upon successful installation".

There is a second tool worth knowing about with a caveat attached. jenkins-plugin-cli, the Plugin Installation Manager Tool, is what most images use:

RUN jenkins-plugin-cli --plugins "docker-workflow json-path-api"
Enter fullscreen mode Exit fullscreen mode

That line is from Jenkins' Docker installation page, and that page is the only place in Jenkins' documentation it appears. The plugin management chapter does not describe the tool at all. Its own repository says it downloads "plugins and their dependencies into a folder" and uses "update center data to get the latest information about a plugin's dependencies".

For the whole configuration rather than just plugins, Configuration as Code is the declarative answer: "an opinionated way to configure Jenkins based on human-readable declarative configuration files", which its page frames as "just translating into code a configuration process one is used to executing in the web UI". Worth noting it says declarative configuration files rather than YAML, so YAML is accurate in substance and is not a quotation.

One honest gap. I wanted to cite a Jenkins instruction to keep plugins updated for security, and the plugin management chapter has none. What exists is the advisory machinery: "Security advisories are the primary way to publicly inform Jenkins users", and "Jenkins administrators are informed about published security issues directly in Jenkins". The advice to act on those is mine, not theirs.

Task 2: Close one container

The task: convert devops-container-111929086 to private, and leave devops-priv-111929086 alone.

Step 1: Establish the negative first

curl -s -o /dev/null -w "%{http_code}\n" \
  "https://$SA.blob.core.windows.net/$PUB?restype=container&comp=list"
curl -s -o /dev/null -w "%{http_code}\n" \
  "https://$SA.blob.core.windows.net/$PRIV?restype=container&comp=list"
Enter fullscreen mode Exit fullscreen mode
PUB before:  200
PRIV before: 404
Enter fullscreen mode Exit fullscreen mode

This is the step that makes everything after it mean something. A 404 at the end could be a wrong container name, a typo in the URL or an account that does not exist. A 404 that was a 200 five minutes ago, from the identical command, is the change itself.

The second line earns its place too. It confirms the already-private container behaves as expected, and gives a known-good reference for what private looks like over HTTP from this client.

Step 2: Pick the narrow control

az storage container set-permission --name "$PUB" --account-name "$SA" \
  --account-key "$KEY" --public-access off
Enter fullscreen mode Exit fullscreen mode

The other option was one command away:

az storage account update -g "$RG" -n "$SA" --allow-blob-public-access false
Enter fullscreen mode Exit fullscreen mode

That one closes every container in the account, now and in future, and Microsoft is explicit about the relationship: "Disallowing anonymous access for a storage account overrides the access settings for all containers in that storage account", and "If you disallow anonymous access for the account, you don't also need to disable anonymous access for individual containers."

It would also have passed the grader. The task said to leave the other container unchanged; it is already private, so nothing visible would change about it. The account flag still alters the conditions it operates under, and the requirement named one container.

The principle is the one from the Jenkins half: match the scope of the change to the scope of the requirement. A control that also fixes things you were not asked about is not a better fix.

A claim I had to drop

I had a second argument for the narrow control, and it does not hold up. I had written that closing at the account level leaves publicAccess: container on the container itself, so the container's own property would still read public and the requirement would not be met.

Microsoft describes the override entirely in terms of effective access, and never says whether the stored container property is retained or cleared. The nearest indirect evidence is that the same article still offers a per-container audit command, which only makes sense if the property persists independently of the account flag. That is an inference. It is not a documented behaviour and I should not have written it as one.

The scope argument stands on its own and did not need the second one.

Step 3: Prove it, and watch a column vanish

PUB after:   404
Enter fullscreen mode Exit fullscreen mode

Then the table:

Name                        PublicAccess
--------------------------  --------------
devops-container-111929086  container
devops-priv-111929086
Enter fullscreen mode Exit fullscreen mode

became:

Name
--------------------------
devops-container-111929086
devops-priv-111929086
Enter fullscreen mode Exit fullscreen mode

The PublicAccess column did not go blank. It disappeared. The CLI's table renderer only adds a key to a row when its value is not null, so once both containers were private, every value in that column was null and the column stopped existing.

Which is uncomfortable, because the disappearance is the result. Both containers being private is exactly what was wanted, and the output announcing it looks like a broken query.

az storage container list --account-name $SA --account-key $KEY \
  --query "[].{Name:name,PublicAccess:properties.publicAccess}" -o json
Enter fullscreen mode Exit fullscreen mode

That returns an explicit "PublicAccess": null for each. Use -o json when the expected answer is an absence, and -o table only for scanning values you expect to be present.

What the docs recommend

Use the conditional headers this operation actually supports. I had recommended --if-match to avoid racing another process adjusting permissions, and that header is not available here. The conditional headers table lists Set Container ACL with If-Modified-Since and If-Unmodified-Since only, and the CLI matches it:

az storage container set-permission --name "$PUB" --account-name "$SA" --account-key "$KEY" \
  --public-access off --if-unmodified-since 2026-09-28T00:00:00Z
Enter fullscreen mode Exit fullscreen mode

Do not read a null in the CLI output as an absence in the API. My response showed "LastModified": null, and the REST reference lists both ETag and Last-Modified among Set Container ACL's response headers, with a sample response carrying both. The last-modified time genuinely advances here: "Any operation that modifies the container or its properties or metadata updates the last-modified time, including setting the container's permissions."

Do not quote a propagation time for this. I had written that the change is immediate. Microsoft documents no propagation figure for a container-level public access change. There are two nearby 30-second figures, one for the account-level anonymous access setting and one for establishing a stored access policy, and neither is this. Borrowing one would be inventing a number.

Plan for what breaks, because nothing warns you. Every blob URL that worked a moment ago now fails for anonymous callers: pages embedding images, documentation links, a partner integration, a CDN with a stale origin. Azure has no record of who was using those URLs, so there is no list to notify.

The scoped replacement is a shared access signature:

az storage blob generate-sas --account-name "$SA" --container-name "$PUB" --name <blob> \
  --permissions r --expiry 2026-09-29T00:00:00Z --account-key "$KEY"
Enter fullscreen mode Exit fullscreen mode

--expiry "Specifies the UTC datetime (Y-m-d'T'H:M'Z') at which the SAS becomes invalid", which is the property a public container can never have.

What I learned

Both halves had a bigger lever within reach, and in both cases pulling it would have passed. Installing a comfortable handful of plugins works. Disabling anonymous access account-wide works. Neither is what was asked for, and the difference only shows up later, when somebody wonders why a container they never touched behaves differently or why a controller is running code nobody chose.

The part I did not expect was having to give up an argument I liked. My second reason for preferring the narrow Azure control turned out to be undocumented, which left the first reason doing all the work. It was enough on its own, which is the useful thing to find out before publishing rather than after.

So here is the Day 69 question. The last time you fixed something, did you fix the thing you were asked about, or everything adjacent to it that looked wrong on the way past?

Day 69 down. Thirty-one to go.

Top comments (0)