Docker Image’larının HTTP API Kullanılarak Silinmesi

Okuma Süresi: 1 dakika

Merhabalar.

Bu yazımda private container registry’de bulunan Docker image’larının Docker’ın HTTP API’ını kullanarak nasıl silineceğinden bahsedeceğim.

Image’ların silinmesi 2 adımdan oluşmaktadır. İlk adım image ismi ve tag değerinin digest değerini almak, ikinci adım ise digest değerini API’a göndererek Docker üzerinden image’ın silinmesi ile sağlamak.

Digest değerini almak için öncelikle container registry’e aşağıdaki gibi bir HTTP isteği göndermek gerekmektedir.

GET /v2/exampleimage/manifests/latest HTTP/1.1
Host: cr.example.com
Accept: application/vnd.docker.distribution.manifest.v2+json
Authorization: Basic YXNkOmFzZA==
User-Agent: PostmanRuntime/7.15.0
Cache-Control: no-cache

Container registry’e bağlanmak için Basic Authentication kullanılmaktadır. Authorization header’ında ilgili kullanıcının adı ve şifre değeri gönderilmelidir. HTTP isteğinin path bölümünde bulunan exampleimage ise silinmek istenen image’ın ismidir. Path’in son kısmında yazan latest ise silinmek istenen image’ın sürümüdür.

İsteği gönderdikten sonra aşağıdakine benzer bir cevap gelmelidir.

{
    "schemaVersion": 2,
    "mediaType": "application/vnd.docker.distribution.manifest.v2+json",
    "config": {
        "mediaType": "application/vnd.docker.container.image.v1+json",
        "size": 4617,
        "digest": "sha256:9f86d081884c7d659a2feaa0c55ad015a3bf4f1b2b0b822cd15d6c15b0f00a08"
    },
    "layers": [
        {
            "mediaType": "application/vnd.docker.image.rootfs.diff.tar.gzip",
            "size": 22496034,
            "digest": "sha256:9f86d081884c7d659a2feaa0c55ad015a3bf4f1b2b0b822cd15d6c15b0f00a08"
        },
        {
            "mediaType": "application/vnd.docker.image.rootfs.diff.tar.gzip",
            "size": 17694102,
            "digest": "sha256:9f86d081884c7d659a2feaa0c55ad015a3bf4f1b2b0b822cd15d6c15b0f00a08"
        },
        {
            "mediaType": "application/vnd.docker.image.rootfs.diff.tar.gzip",
            "size": 2977123,
            "digest": "sha256:9f86d081884c7d659a2feaa0c55ad015a3bf4f1b2b0b822cd15d6c15b0f00a08"
        },
        {
            "mediaType": "application/vnd.docker.image.rootfs.diff.tar.gzip",
            "size": 62093437,
            "digest": "sha256:9f86d081884c7d659a2feaa0c55ad015a3bf4f1b2b0b822cd15d6c15b0f00a08"
        },
        {
            "mediaType": "application/vnd.docker.image.rootfs.diff.tar.gzip",
            "size": 136,
            "digest": "sha256:9f86d081884c7d659a2feaa0c55ad015a3bf4f1b2b0b822cd15d6c15b0f00a08"
        },
        {
            "mediaType": "application/vnd.docker.image.rootfs.diff.tar.gzip",
            "size": 18044387,
            "digest": "sha256:9f86d081884c7d659a2feaa0c55ad015a3bf4f1b2b0b822cd15d6c15b0f00a08"
        }
    ]
}

Digest değerimiz gelen cevaptaki 7. satırda bulunmaktadır.

Digest değerimizi aldığımıza göre artık image’ı silme işlemine geçebiliriz. Silme işlemini gerçekleştirmek için aşağıdaki gibi bir HTTP isteği göndermemiz gerekmektedir.

DELETE /v2/exampleimage/manifests/sha256:9f86d081884c7d659a2feaa0c55ad015a3bf4f1b2b0b822cd15d6c15b0f00a08 HTTP/1.1
Host: cr.example.com
Accept: application/vnd.docker.distribution.manifest.v2+json
Authorization: Basic YXNkOmFzZA==
User-Agent: PostmanRuntime/7.15.0
Cache-Control: no-cache

Yukarıdaki örnek HTTP isteğinde görülebileceği gibi http://cr.example.com//v2/exampleimage/manifests/sha256:9f86d081884c7d659a2feaa0c55ad015a3bf4f1b2b0b822cd15d6c15b0f00a08 adresine DELETE isteği atmamız yeterlidir. Adresimizin sonunda sha256 ile başlayan ifade, bizim image’ımızın digest değeridir.

İsteği gönderdikten sonra herhangi bir cevap gelmeyecektir. İşlemin başarılı olup olmadığını gelen HTTP durum kodundan anlayabiliriz. Gelen durum kodu HTTP 202 Accepted ise image silme işlemi başarıyla gerçekleşmiş demektedir.

Bu işlemleri otomatik yapan .Net Core projesine https://github.com/mennan/DockerImageRemover adresinden ulaşabilirsiniz.

.Net Core’da HttpClient ile Proxy Kullanımı

Okuma Süresi: 2 dakika

.Net Core ile geliştirdiğimiz uygulamalarda HttpClient sınıfını kullanarak uç noktalara istek atmamız gerekebilir. Örnek olarak kurumsal firmalarda çalışacak olan uygulamanız internet ortamındaki bir adrese istek göndermek isteyebilir. Ancak uygulamanızın çalışmış olduğu sunucunun internet erişimi kısıtlandığından dolayı ilgili adrese erişemeyebilirsiniz veya HttpClient sınıfı kullanarak göndermiş olduğunuz isteği ve gelen cevabı Charles Web Debugging Proxy gibi uygulamalar ile araya girerek takip etmek isteyebilirsiniz. Bu işlemleri yapabilmeniz için HttpClient ile birlikte proxy kullanmanız gerekmektedir.

HttpClient ile atılan isteklerde proxy tanımı yapabilmek için HttpClientHandler ve WebProxy sınıflarına ihtiyacımız vardır.

Örnek uygulama olarak .Net Core’da bir console projesi oluşturacağız. Aşağıda bash komutları ile projemizi oluşturalım.

dotnet new console -n NetCoreHttpClientProxy
cd NetCoreHttpClientProxy

Projemizi kullanmış olduğumuz metin editörü veya IDE ile açarak aşağıdaki şekilde kodlarımızı yazalım.

using System;
using System.Net;
using System.Net.Http;
using System.Threading.Tasks;

namespace NetCoreHttpClientProxy
{
	class Program
	{
		static async Task Main(string[] args)
		{
			const string ProxyUrl = "http://localhost:5001";
			const string ProxyUsername = "";
			const string ProxyPassword = "";
			const string RequestUrl = "https://google.com";

			var proxy = new WebProxy
			{
				Address = new Uri(ProxyUrl),
				Credentials = new NetworkCredential(ProxyUsername, ProxyPassword)
			};

			var httpClientHandler = new HttpClientHandler
			{
				Proxy = proxy,
				UseProxy = true
			};

			using (var client = new HttpClient(httpClientHandler))
			{
				var response = await client.GetAsync(RequestUrl);

				if (response.IsSuccessStatusCode)
				{
					var responseString = await response.Content.ReadAsStringAsync();
					Console.WriteLine(responseString);
				}
			}
		}
	}
}

Yapmış olduğumuz işlemleri açıklamak gerekirse https://google.com adresine http://localhost:5001 proxy adresini kullanarak istek göndermekteyiz.

Öncelikle WebProxy sınıfını kullanarak proxy adresini ve proxy’ye ait kullanıcı adı ve şifre tanımlamalarını yapıyoruz. Eğer proxy’ye ait herhangi bir kullanıcı adı ve şifre bilgisi bulunmuyorsa Credentials özelliğine değer vermenize gerek yoktur.

Sonrasında ise oluşturmuş olduğumuz WebProxy nesnesini HttpClientHandler tipindeki bir nesnenin Proxy özelliğine atıyoruz ve ilgili nesne örneğinin UseProxy özelliğini true olarak ayarlıyoruz.

En son işlem olarak ise HttpClient tipinin nesne örneğini oluştururken yapıcı metoda (constructor) HttpClientHandler tipindeki nesne örneğini vermek. Bu aşamadan sonra HttpClient tipindeki nesne örneğinden gönderilecek işlem proxy üzerinden geçerek atılacaktır.

Örnek kodlara https://github.com/mennan/netcore-httpclient-proxy adresinden ulaşabilirsiniz.

Entity Framework Core’da Global Query Filters Kullanımı

Okuma Süresi: 4 dakika

Global Query Filters, entity nesnelerinde özellikle Where sorgu operatörüyle gönderdiğimiz sorguların her sorguya otomatik olarak eklemek için kullanılır. Kullanım senaryolarına örnek vermek gerekirse soft delete veya multi tenancy sorguları verilebilir. Bu yazıda örnek olması açısından soft delete senaryosu üzerinden ilerleyeceğiz. Global Query Filter tanımları DbContext sınıfının virtual olan OnModelCreating metodunda tanımlanır.

Bu uygulamamızda yapacağımız örnekte Users tablosunda IsRemoved kolonunun değeri false olan kullanıcıların listesinin gelmesi sağlanacaktır. Global Query Filter kullanmadan önce yapacağımız sorgu aşağıki gibidir.

context.Users.Where(u => !u.IsRemoved).ToList();

Normalde bu kullanım doğrudur. Ancak kodumuzun birçok yerinde Users tablosundan veri çekerken Where ile sürekli IsRemoved kontrolünün yapılması gerekmektedir. Bu işlemde kodun bakım maliyetini artırmaktadır. Bu işlemi Global Query Filter ile otomatikleştirerek kodumuzu aşağıdaki şekilde kullanmaya başlayacağız.

context.Users.ToList();

Şimdi uygulamamızı geliştirmeye başlayabiliriz. Örnek uygulama için ASP.NET Core MVC projesi oluşturacağım. Aşağıdaki komutu terminale yazarak yeni bir proje oluşturuyorum.

dotnet new mvc -n EFCore.GlobalQueryFiltersSample

İlk önce projeme IRemovable isminde bir interface ekliyorum ve içeriğini aşağıdaki şekilde düzenliyorum.

public interface IRemovable
{
    bool IsRemoved { get; set; }
}

Projeme SampleDbContext isminde bir sınıf ekliyorum ve içeriğini aşağıdaki şekilde değiştiriyorum.

using System;
using System.ComponentModel.DataAnnotations;
using System.ComponentModel.DataAnnotations.Schema;
using Microsoft.EntityFrameworkCore;

namespace EFCore.GlobalQueryFiltersSample
{
    [Table("Users")]
    public class User : IRemovable
    {
        [Key]
        public Guid UserId { get; set; }
        public string UserName { get; set; }
        public string Email { get; set; }
        public string Name { get; set; }
        public bool IsRemoved { get; set; }
    }

    public class SampleDbContext : DbContext
    {
        public DbSet<User> Users { get; set; }

        public SampleDbContext(DbContextOptions<SampleDbContext> options) : base(options)
        {
        }

        protected override void OnModelCreating(ModelBuilder modelBuilder)
        {
            modelBuilder.Entity<User>().HasQueryFilter(u => !u.IsRemoved);
            base.OnModelCreating(modelBuilder);
        }
    }
}

Global Query Filter tanımını SampleDbContext sınıfının 29. satırında yaptık. Global Query Filter tanımını HasQueryFilter metodu aracılığıyla yapmaktayız. Parametre olarak aldığı Expression ile sorgu tanımımızı gerçekleştirdik. Bu sorgu tanımına göre User tipindeki nesnelere ait verileri getirirken IsRemoved alanındaki değeri false olanları getirmesini sağladık.

Bu işlemleri yaptıktan sonra Entity Framework’ün ayarlarını yapmaya geldi. Ben bu yazı için InMemory database kullanacağım. Entity framework’ü InMemory database ile kullanabilmek için Microsoft.EntityFrameworkCore.InMemory paketini projeye eklememiz gerekmektedir. Projeye paketi eklemek için aşağıdaki komutu terminale yazmamız gerekmektedir.

dotnet add package Microsoft.EntityFrameworkCore.InMemory --version 2.2.4

İlgili paketi projeye ekledikten sonra DbContext’imizi ASP.NET Core uygulamıza tanıtmak kaldı. İlgili işlemi Startup.cs dosyamızda yapacağız. Startup.cs dosyamızın içeriği aşağıdaki gibidir.

using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Hosting;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.HttpsPolicy;
using Microsoft.AspNetCore.Mvc;
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;

namespace EFCore.GlobalQueryFiltersSample
{
    public class Startup
    {
        public Startup(IConfiguration configuration)
        {
            Configuration = configuration;
        }

        public IConfiguration Configuration { get; }

        // This method gets called by the runtime. Use this method to add services to the container.
        public void ConfigureServices(IServiceCollection services)
        {
            services.Configure<CookiePolicyOptions>(options =>
            {
                // This lambda determines whether user consent for non-essential cookies is needed for a given request.
                options.CheckConsentNeeded = context => true;
                options.MinimumSameSitePolicy = SameSiteMode.None;
            });

            services.AddDbContext<SampleDbContext>(options => options.UseInMemoryDatabase("Sample"));
            services.AddMvc().SetCompatibilityVersion(CompatibilityVersion.Version_2_2);
        }

        // This method gets called by the runtime. Use this method to configure the HTTP request pipeline.
        public void Configure(IApplicationBuilder app, IHostingEnvironment env)
        {
            if (env.IsDevelopment())
            {
                app.UseDeveloperExceptionPage();
            }
            else
            {
                app.UseExceptionHandler("/Home/Error");
                // The default HSTS value is 30 days. You may want to change this for production scenarios, see https://aka.ms/aspnetcore-hsts.
                app.UseHsts();
            }

            app.UseHttpsRedirection();
            app.UseStaticFiles();
            app.UseCookiePolicy();

            app.UseMvc(routes =>
            {
                routes.MapRoute(
                    name: "default",
                    template: "{controller=Home}/{action=Index}/{id?}");
            });
        }
    }
}

Şimdi controller’ımıza geçip uygulamamızı test etmeye geldi. Ben HomeController üzerinde çalışacağım. İçeriği aşağıdaki gibidir.

using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Linq;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Mvc;
using EFCore.GlobalQueryFiltersSample.Models;

namespace EFCore.GlobalQueryFiltersSample.Controllers
{
    public class HomeController : Controller
    {
        private readonly SampleDbContext _context;

        public HomeController(SampleDbContext context)
        {
            _context = context;

            var users = new List<User>
                {
                    new User {
                        UserId = Guid.NewGuid(),
                        UserName = "mennan",
                        Name = "Mennan",
                        IsRemoved = false
                    },
                    new User {
                        UserId = Guid.NewGuid(),
                        UserName = "anil",
                        Name = "Anıl",
                        IsRemoved = true
                    }
                };

            _context.Users.AddRange(users);
            _context.SaveChanges();
        }

        public IActionResult Index()
        {
            var users = _context.Users.ToList();

            return View(users);
        }

        [ResponseCache(Duration = 0, Location = ResponseCacheLocation.None, NoStore = true)]
        public IActionResult Error()
        {
            return View(new ErrorViewModel { RequestId = Activity.Current?.Id ?? HttpContext.TraceIdentifier });
        }
    }
}

Controller’ın constructor metodunda DbContext’imizin instance’ı DI ile alıyoruz ve örnek verilerimizi veritabanına kaydediyoruz. Index action’ında ise veritabanındaki tüm kayıtların listesini alıyoruz. Şimdi Index view’ın içeriğini aşağıdaki şekilde değiştiriyoruz.

@model List<User>
@{
    ViewData["Title"] = "Home Page";
}

<ul>
    @foreach (var user in Model)
    {
        <li>
            @user.Name - @user.UserName
        </li>
    }
</ul>

Gerekli değişiklikleri yaptıktan sonra sıra uygulamamızı test etmeye geldi. Terminalden dotnet run komutunu yazarak uygulamamızı tarayıcı üzerinden açalım.

Normalde veritabanımızda 2 adet kayıt olduğu halde sadece IsRemoved değeri false olan kayıt gelmiştir.

Bazı durumlarda GlobalQueryFilter’ı pasif hale getirmemiz gerekebilir. Bunun için LINQ sorgunuza IgnoreQueryFilters metodunu eklemeniz gerekmektedir. Örnek kullanımı aşağıdaki gibidir.

context.Users.IgnoreQueryFilters().ToList();

IRemovable’dan türeyen tüm entity tipleri için tek tek Global Query Filter tanımı yapmamıza gerek yok. Yapmamız gereken reflection ile IRemovable arayüzünden türeyen tipleri bulup bunları DbContext’in OnModelCreating metodunda dinamik olarak eklemek. Bunun için SampleDbContext sınıfımızı aşağıdaki şekilde düzenliyoruz.

using System;
using System.ComponentModel.DataAnnotations;
using System.ComponentModel.DataAnnotations.Schema;
using System.Linq;
using System.Linq.Expressions;
using System.Reflection;
using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Metadata;

namespace EFCore.GlobalQueryFiltersSample
{
    [Table("Users")]
    public class User : IRemovable
    {
        [Key]
        public Guid UserId { get; set; }
        public string UserName { get; set; }
        public string Email { get; set; }
        public string Name { get; set; }
        public bool IsRemoved { get; set; }
    }

    public class SampleDbContext : DbContext
    {
        public DbSet<User> Users { get; set; }
        private static readonly MethodInfo ConfigureGlobalFiltersMethodInfo = typeof(SampleDbContext).GetMethod(nameof(ConfigureGlobalFilters), BindingFlags.Instance | BindingFlags.NonPublic);

        public SampleDbContext(DbContextOptions<SampleDbContext> options) : base(options)
        {
        }

        protected override void OnModelCreating(ModelBuilder modelBuilder)
        {
            foreach (var entityType in modelBuilder.Model.GetEntityTypes())
            {
                ConfigureGlobalFiltersMethodInfo
                    .MakeGenericMethod(entityType.ClrType)
                    .Invoke(this, new object[] { modelBuilder, entityType });
            }

            base.OnModelCreating(modelBuilder);
        }

        protected void ConfigureGlobalFilters<TEntity>(ModelBuilder modelBuilder, IMutableEntityType entityType) where TEntity : class
        {
            if (entityType.BaseType != null || !ShouldFilterEntity<TEntity>(entityType)) return;
            var filterExpression = CreateFilterExpression<TEntity>();
            if (filterExpression == null) return;
            if (entityType.IsQueryType)
                modelBuilder.Query<TEntity>().HasQueryFilter(filterExpression);
            else
                modelBuilder.Entity<TEntity>().HasQueryFilter(filterExpression);
        }

        protected virtual bool ShouldFilterEntity<TEntity>(IMutableEntityType entityType) where TEntity : class
        {
            return typeof(IRemovable).IsAssignableFrom(typeof(TEntity));
        }

        protected Expression<Func<TEntity, bool>> CreateFilterExpression<TEntity>() where TEntity : class
        {
            Expression<Func<TEntity, bool>> expression = null;

            if (typeof(IRemovable).IsAssignableFrom(typeof(TEntity)))
            {
                Expression<Func<TEntity, bool>> removedFilter = e => !((IRemovable)e).IsRemoved;
                expression = expression == null ? removedFilter : CombineExpressions(expression, removedFilter);
            }

            return expression;
        }

        protected Expression<Func<T, bool>> CombineExpressions<T>(Expression<Func<T, bool>> expression1, Expression<Func<T, bool>> expression2)
        {
            return ExpressionCombiner.Combine(expression1, expression2);
        }
    }
}

DbContext’i bu şekilde düzenlediğimiz zaman artık elle IRemovable için elle tekrardan tanıtma işlemi yapmamıza gerek kalmamıştır.

Yazıda bahsi geçen uygulamanın kaynak kodlarına https://github.com/mennan/efcore-globalqueryfilters-sample adresinden ulaşabilirsiniz.